Autofac ComponentRegistryBuilder in ASP.NET Core - How To Register Dependencies (Part 3)

In this article, we'll be exploring how to use Autofac ComponentRegistryBuilder in ASP.NET Core. Prior articles in this series have highlighted some challenges for getting set up to do C# plugin architectures -- at least for my own standards. I'll be walking us through how this approach can help overcome some of the challenges that have previously been highlighted.

This will be part of a series where I explore dependency resolution with Autofac inside of ASP.NET Core. I’ll be sure to include the series below as the issues are published:

At the end of this series, you’ll be able to more confidently explore plugin architectures inside of ASP.NET Core and Blazor — which will be even more content for you to explore. Keep your eyes peeled on my Dometrain courses for a more guided approach to these topics later in 2023.


What's In This Article: Autofac ComponentRegistryBuilder in ASP.NET Core

Remember to check out these platforms:

// FIXME: social media icons coming back soon!


Where Did We Leave Off With Autofac in ASP.NET Core?

The previous two articles looked at the following scenarios:

  • Setting up using the AutofacServiceProviderFactory as the standard recommended approach
  • Skipping AutofacServiceProviderFactory and using Autofac ContainerBuilder directly

In both cases, we were able to get a web application up and running using Autofac for dependency injection. However, both of these had limitations around:

  • Accessing the WebApplication instance on the container
  • Weird nuances with minimal API support

While both options are absolutely viable -- and may be great for you given your constraints -- I wanted to push a bit further to see if the wrinkles could be ironed out. I want to strive towards having configuration done in separate Autofac modules and pushing towards a C# plugin architecture for the majority of my application development.


Exploring A Sample ASP.NET Core Application

This one is going to be different than the previous articles -- we've achieved plugin status. I want to show you how the code from the previous examples can now be broken out into more dedicated pieces. Most of what we've gone over before is the same concept, but I've reduced the weather route to something more contrived just to eliminate the waste.

Make sure to follow along with this video on Autofac for additional explanations as we go through:

The Entry Point Configuration

Here's how simple our Program.cs file is now:

await new FullResolveWebApi().RunAsync(CancellationToken.None);

That's right -- one line of code. But okay, you're probably curious where all the setup actually takes place. Let's go a bit deeper:

using Autofac;

internal sealed class FullResolveWebApi
{
    public async Task RunAsync(CancellationToken cancellationToken)
    {
        var containerBuilder = new MyContainerBuilder();
        using var container = containerBuilder.Build();
        using var scope = container.BeginLifetimeScope();
        
        var app = scope
            .Resolve<ConfiguredWebApplication>()
            .WebApplication;
        await app.RunAsync(cancellationToken).ConfigureAwait(false);
    }
}

This looks familiar to what we saw in the previous example! We're able to get the goodness of that really lean startup configuration But wait! What's that custom MyContainerBuilder class?!

using Autofac;

using System.Reflection;

internal sealed class MyContainerBuilder
{
    public IContainer Build()
    {
        ContainerBuilder containerBuilder = new();

        // TODO: do some assembly scanning if needed
        var assembly = Assembly.GetExecutingAssembly();
        containerBuilder.RegisterAssemblyModules(assembly);

        var container = containerBuilder.Build();
        return container;
    }
}

This is missing from the code above if we compare it to the previous article, but it can also be extended to do assembly scanning if that's a requirement. So far, so good. We have one more piece though, and that's ConfiguredWebApplication:

internal sealed class ConfiguredWebApplication(
    WebApplication _webApplication,
    IReadOnlyList<PreApplicationConfiguredMarker> _markers)
{
    public WebApplication WebApplication => _webApplication;
}

internal sealed record PreApplicationBuildMarker();

internal sealed record PreApplicationConfiguredMarker();

This marker record might seem a bit confusing but we'll tie this all together in a dedicated section.

WebApplicationBuilder Autofac Module

Now that we've seen how our initial ASP NET Core application bootstrap code is looking, it's time to look at some of the core dependency registration that's going to be a union of what we saw in the previous articles AND some new behavior:

using Autofac;
using Autofac.Extensions.DependencyInjection;

internal sealed class WebApplicationBuilderModule : global::Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder
            .Register(ctx =>
            {
                var builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs());
                return builder;
            })
            .SingleInstance();
        builder
            .Register(ctx =>
            {
                var config = ctx.Resolve<WebApplicationBuilder>().Configuration;
                return config;
            })
            .As<IConfiguration>()
            .SingleInstance();

        WebApplication? cachedWebApplication = null;
        builder
            .Register(ctx =>
            {
                if (cachedWebApplication is not null)
                {
                    return cachedWebApplication;
                }

                var webApplicationBuilder = ctx.Resolve<WebApplicationBuilder>();
                ctx.Resolve<IReadOnlyList<PreApplicationBuildMarker>>();

                webApplicationBuilder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(containerBuilder =>
                {
                    foreach (var registration in ctx.ComponentRegistry.Registrations)
                    {
                        containerBuilder.ComponentRegistryBuilder.Register(registration);
                    }

                    containerBuilder
                        .RegisterInstance(webApplicationBuilder)
                        .SingleInstance();
                }));

                cachedWebApplication = webApplicationBuilder.Build();
                return cachedWebApplication;
            })
            .SingleInstance();
        builder
            .Register(ctx =>
            {
                var app = ctx.Resolve<WebApplication>();
                app.UseHttpsRedirection();
                return new PreApplicationConfiguredMarker();
            })
            .SingleInstance();
        builder
            .RegisterType<ConfiguredWebApplication>()
            .SingleInstance();
    }
}

You'll notice that the code snippet above shows that we're now mixing in the AutofacServiceProviderFactory alongside our standalone Autofac ContainerBuilder approach. What we're able to do is leverage this code to re-register some of the dependency registrations on the second Autofac ContainerBuilder:

foreach (var registration in ctx.ComponentRegistry.Registrations)
{
    containerBuilder.ComponentRegistryBuilder.Register(registration);
}

Now that we can duplicate our registrations, we get the registration of the WebApplication from the parent container onto the dedicated WebApplication's ContainerBuilder. But two things we should note:

  • We need to cache the WebApplication instance. This is because later on when the WebApplication instance itself needs to resolve dependencies that depend on an instance of WebApplication, it will go re-run the registration *even though it's a single instance*! This is because this is a duplicated registration across the container that has never technically been executed at the time of registration. We may need to pay special attention to this sort of thing as we go forward to avoid expensive re-resolution of types.
  • We see another marker type: PreApplicationConfiguredMarker. What's with these markers?!

Marker Classes for Controlling Dependency Ordering and Requirements

So far we've seen two instances of marker types. These marker types are a way that we can force certain registration code to execute before some other registration code executes. This is a more flexible way of saying "I don't care which types specifically get registered or which registration code runs, but anyone that needs to be registered before some checkpoint, make sure you return one of these". This allows us to force code to execute before a checkpoint.

If we consider the code in the example above, we see that the ConfiguredWebApplication instance requires the full collection of PreApplicationConfiguredMarker instances. This means that we can't even create an instance of ConfiguredWebApplication until all dependent code, as indicated by our marker type, has finished executing. This essentially forces Autofac to run certain code for us because it will attempt to run all code that provides one of these marker instances.

The two markers we see in this example code are very naive/primitive -- however, this concept can be expanded to provide more robust checkpoints in your dependency registration process.


C# Plugin Architecture in ASP.NET Core Unlocked!

The cat's out of the bag! We can now successfully create an Autofac module for a plugin! This code shows us enabling C# plugin architecture in ASP.NET Core as we're able to add a new discoverable module that adds its own API endpoints:

using Autofac;

namespace Plugins;

internal sealed class PluginModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        // load whatever dependencies you want in your plugin
        // taking note that they will be able to have access
        // to services/dependencies in the main application
        // by default
        builder.RegisterType<DependencyA>().SingleInstance();
        builder.RegisterType<DependencyB>().SingleInstance();
        builder.RegisterType<DependencyC>().SingleInstance();

        // minimal APIs can resolve dependencies from the
        // method signature itself
        builder
            .Register(ctx =>
            {
                var app = ctx.Resolve<WebApplication>();

                app.MapGet(
                    "/hello",
                    (
                        DependencyA dependencyA
                      , DependencyB dependencyB
                      , DependencyC dependencyC
                    ) =>
                    {
                        return new
                        {
                            AAA = dependencyA.ToString(),
                            BBB = dependencyB.ToString(),
                            CCC = dependencyC.ToString(),
                        };
                    });

                // this is a marker to signal a dependency
                // before the application can be considered
                // configured
                return new PreApplicationConfiguredMarker();
            })
            .SingleInstance();
    }
}

internal sealed class DependencyA(
    WebApplicationBuilder _webApplicationBuilder)
{
    public override string ToString() => _webApplicationBuilder.ToString();
}

internal sealed class DependencyB(
    Lazy<WebApplication> _webApplication)
{
    public override string ToString() => _webApplication.Value.ToString();
}

internal sealed class DependencyC(
    IConfiguration _configuration)
{
    public override string ToString() => _configuration.ToString();
}

If you'd like a more full explanation as to what you're seeing, I highly recommend you read the articles linked at the top of this one first just to get an idea of why we have some dependencies set up like this. The TL;DR is that this code demonstrates that we can access some dependencies that are of interest to us when building plugin architectures.

What Benefits For C# Plugin Architecture Have Been Unlocked?

The code examples in this article are essentially marrying approaches from two of the previous articles... so hopefully we have the best of both worlds! Let's have a look:

  • Can access WebApplicationBuilder instance from the WebApplication's dependency container.
  • Can access IConfiguration instance from the WebApplication's dependency container.
  • Can access WebApplication instance from the WebApplication's dependency container.
  • Can resolve Autofac ContainerBuilder dependencies on the minimal APIs directly
  • Can create separate Autofac modules (i.e. for plugin usage) that register minimal APIs directly onto the WebApplication instance
  • Can get an extremely lightweight (one line!) entry point to our application. The core "skeleton" application code is fundamentally just setting up dependencies to be resolved.

All of these are boxes that I wanted to check before continuing to build plugins. With this infrastructure in place, I feel much more confident!

What Gaps Are Left For C# Plugin Architecture?

Of course, we need to look at both pros AND cons when we analyze things. Let's dive in:

  • We have two dependency containers to worry about. In theory, the usage of Autofac ComponentRegistryBuilder in ASP.NET Core should allow us to clone registrations between the two, but more complexity can potentially arise from this as we continue.
  • We saw an interesting need to cache the WebApplication instance to avoid dependency recreation -- are there other scenarios like this we haven't hit yet?
  • One of the general plugin architecture concerns: Being able to configure EVERYTHING with plugins can make some things harder to find and structure. Do we really need these little marker types to help organize ourselves?

Wrapping Up Autofac ComponentRegistryBuilder in ASP.NET Core

Overall, I'm quite happy with how using Autofac ComponentRegistryBuilder in ASP.NET Core has allowed us to progress our dependency injection patterns. This approach which I've highlighted in the article has made it significantly easier for me to go structure plugins the way that I'd like to in a C# plugin architecture. Not without tradeoffs -- but I feel this pattern fits my needs.

If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube! Meet other like-minded software engineers and join my Discord community!