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.
The previous two articles looked at the following scenarios:
AutofacServiceProviderFactory
as the standard recommended approachAutofacServiceProviderFactory
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:
WebApplication
instance on the container
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.
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:
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.
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:
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.PreApplicationConfiguredMarker
. What’s with these markers?!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.
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.
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:
WebApplicationBuilder
instance from the WebApplication
‘s dependency container.IConfiguration
instance from the WebApplication
‘s dependency container.WebApplication
instance from the WebApplication
‘s dependency container.WebApplication
instance
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!
Of course, we need to look at both pros AND cons when we analyze things. Let’s dive in:
WebApplication
instance to avoid dependency recreation — are there other scenarios like this we haven’t hit yet?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!