There are many ways to manage dependency injection inside our applications, and I think it’s important to understand the benefits and limitations of different approaches. Using an Autofac ContainerBuilder
in ASP.NET Core as the primary way to structure your dependencies instead of using the suggested AutofacServiceProviderFactory
is one such path we can explore!
In this article, I highlight how to use Autofac’s ContainerBuilder in your ASP.NET Core application instead of the AutofacServiceProviderFactory. We’ll look at what you can and cannot do with this approach versus the other approaches we have access to with dependency injection.
This will be part of a series where I explore dependency resolution with Autofac inside 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 provide even more content for you to explore.
To be fair, the section title is *almost* click-bait. I do think that AutofacServiceProviderFactory
being used as the suggested way to set up Autofac in your ASP.NET Core applications is great for most applications. The great majority of developers who want to use Autofac as their dependency injection framework of choice would not run into many issues at all this way.
It does afford us the ability to:
WebApplicationBuilder
(and anything available at this point in time)IConfiguration
(also available off the WebApplicationBuilder
instance)
But the big issue for me: We can’t access the WebApplication
instance. When I build plugin architectures in C#, particularly when building ASP.NET Core applications, I like to have access to the WebApplication
instance in order to register routes. This allows me to register minimal APIs from my plugins with ease, which technically only need access to an implementation of IEndpointRouteBuilder
to get the handy syntax.
Can I register non-minimal APIs without this? Absolutely. Is there another way to provide similar syntax and not require a WebApplication
instance? Very likely. But instead of trying to work around THAT problem, I wanted to see if I could just get access to the dependency I am interested in.
It was time to change the plan on how to set up my dependency container!
Let’s look at a sample application so that we have some common ground to explore. If you’ve read the previous article, this will look similar — a variation of the sample weather application:
using Autofac;
using Microsoft.AspNetCore.Mvc;
// personal opinion:
// I absolutely love having the entry point of my
// applications being essentially:
// - make my dependencies
// - give me the primary dependency
// - use it
// - ... nothing else :)
ContainerBuilder containerBuilder = new();
containerBuilder.RegisterModule<MyModule>();
using var container = containerBuilder.Build();
using var scope = container.BeginLifetimeScope();
var app = scope.Resolve<WebApplication>();
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
internal sealed class MyModule : Module
{
protected override void Load(ContainerBuilder containerBuilder)
{
containerBuilder.RegisterType<DependencyA>().SingleInstance();
containerBuilder.RegisterType<DependencyB>().SingleInstance();
containerBuilder.RegisterType<DependencyC>().SingleInstance();
containerBuilder
.Register(ctx =>
{
var builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs());
return builder;
})
.SingleInstance();
containerBuilder
.Register(ctx => ctx.Resolve<WebApplicationBuilder>().Configuration)
.As<IConfiguration>()
.SingleInstance();
containerBuilder
.Register(ctx =>
{
var builder = ctx.Resolve<WebApplicationBuilder>();
var app = builder.Build();
app.UseHttpsRedirection();
// FIXME: the problem is that the Autofac ContainerBuilder
// was used to put all of these pieces together,
// but we never told the web stack to use Autofac as the
// service provider.
// this means that the minimal API will never be able to
// find services off the container. we would need to resolve
// them BEFORE the API is called, like in this registration
// method itself, from the context that is passed in.
//DependencyA dependencyA = ctx.Resolve<DependencyA>();
// FIXME: But... What happens if something wants to take a
// dependency on the WebApplication instance itself? Once the
// web application has been built, there's no more adding
// dependencies to it!
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet(
"/weatherforecast",
(
[FromServices] DependencyA dependencyA
, [FromServices] DependencyB dependencyB
, [FromServices] DependencyC dependencyC
) =>
{
var forecast = Enumerable
.Range(1, 5)
.Select(index => new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
return app;
})
.SingleInstance();
}
}
internal sealed class DependencyA(
WebApplicationBuilder _webApplicationBuilder);
internal sealed class DependencyB(
WebApplication _webApplication);
internal sealed class DependencyC(
IConfiguration _configuration);
One callout of this approach is that using the Autofac ContainerBuilder
class as our primary dependency container affords us the opportunity to structure our entry point to just:
This is, in my opinion, ideal application startup code. Why? Because you never need to come back here to touch it. Ever. No matter how many things you add in! And that’s all because you can scan assemblies to load more modules.
Again, this is a personal preference of mine, and I am not trying to claim that this should be everyone’s goal.
Of course, another approach that isn’t quite bulletproof. So let’s discuss what we DON’T get with this setup of Autofac:
WebApplication
that was built is not configured to use Autofac as the dependency injection framework!WebApplication
instance on the dependency container… So we didn’t make any advancements specifically on accessing that.
But that’s mostly it! It’s not a terrible list of drawbacks, but the Autofac ContainerBuilder approach was not a silver bullet solution for us. So, what did we get out of it? will also help explain the following:
Pros and cons for everything we do! Now that we’ve seen the issues with Autofac ContainerBuilder in ASP.NET Core, it’s time to look at what advancements we got out of this:
WebApplicationBuilder
and IConfiguration
instances, so that’s a comparable benefit to using the AutofacServiceProviderFactory
approach.We saw that we could register minimal APIs within an Autofac registration method, but unfortunately, we cannot resolve dependencies from the container directly on the minimal API call itself. We could go build a dedicated class like the following that handles route definitions with dependencies being resolved automatically:
internal sealed class WeatherForecastRoutes(
DependencyA _dependencyA
// FIXME: still can't depend on this because
// we can't get the WebApplication
//, DependencyB _dependencyB
, DependencyC _dependencyC)
{
private static readonly string[] _summaries = new[]
{
"Freezing", "Bracing", // ...
};
public WeatherForecast[] Forecast()
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
_summaries[Random.Shared.Next(_summaries.Length)]
))
.ToArray();
return forecast;
}
}
The automatic resolution happens if we have this class AND the dependencies all on the same container. Then it’s just a matter of calling this code:
var weatherForecastRoutes = ctx.Resolve<WeatherForecastRoutes>();
app.MapGet("/weatherforecast2", weatherForecastRoutes.Forecast);
This still sucks a bit from a plugin perspective because we’d need to go resolve all of the route classes manually just to call the registration code like that — all stemming from the fact that these things can’t resolve their own access to the WebApplication
instance.
But wait… What if we flip things around? What if we could resolve some interface like IRegisterRoutes
and pass in the WebApplication
instance?!
// NOTE: make sure to register WeatherForecastRouteRegistrar on the
// autofac container as IRegisterRoutes!
internal interface IRegisterRoutes
{
void RegisterRoutes(WebApplication app);
}
internal sealed class WeatherForecastRouteRegistrar(
WeatherForecastRoutes _weatherForecastRoutes) :
IRegisterRoutes
{
public void RegisterRoutes(WebApplication app)
{
app.MapGet("/weatherforecast2", _weatherForecastRoutes.Forecast);
}
}
// TODO: add this to the autofac code where the
// WebApplication instance is built:
foreach (var registrar in ctx.Resolve<IEnumerable<IRegisterRoutes>>())
{
registrar.RegisterRoutes(app);
}
Now, we don’t even need to care if the WebApplication
instance is accessible to our plugins! Maybe the first version wasn’t so limiting after all. Maybe we’re onto something here… However, the next article should explain this in more detail.
In this article, I explored using an Autofac ContainerBuilder
explicitly instead of using AutofacServiceProviderFactory
as is normally suggested. We saw some similar benefits and drawbacks, but also a different set of things to consider. Each way can offer pros and cons depending on what you’re after in your application.
What was interesting was that if we’re trying to work towards plugins, we might not even need to access the WebApplication
instance from our plugins at all! If we care about minimal APIs, this might still be limiting… but otherwise, we’re onto an interesting line of thinking!
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!