Dependency Injection In .NET With Autofac

In my previous article about Dependency Injection In .NET (dotnet core,dotnet 5,dotnet 6) I have provided details about default IoC container that present in .NET core.

In this article, I will extend that part with some more detail and advanced topic. I will cover how can we use other IoC container like Autofac in .NET core. Though main focus would be on Autofac but this can be useful for other IoC container as well.

It is quite easy to replace existing container with new Autofac IoC container. This is due to Confirming Container. More information on this at link. Basically those container also implement same interface that is being used by default IoC container comes up with .net.

For this article I am using following tools.

  • Visual Studio 2022 Community Edition
  • C# as Language
  • .NET 6 Sdk/Runtime

Let's start without wasting much time.

  • For sample services, I am taking same example that I took earlier.
public interface IService
{
    string GetMessage();
}

public class ServiceA : IService
{
    public string GetMessage()
    {
        return "Message from ServiceA.";
    }
}

public class ServiceB : IService
{
    public string GetMessage()
    {
        return "Message from ServiceB.";
    }
}
  • Create minimal webapi project. Program.cs looks something like this.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IService, ServiceA>();
builder.Services.AddScoped<IService, ServiceB>();
var app = builder.Build();

app.MapGet("/hello", (IService service) =>
{
    return service.GetMessage();
}).WithName("Hello");

app.Run("http://localhost:5000");
  • Run the service and hit the url http://localhost:5000/hello
output:
Message from ServiceB.
  • Once done install nuget package for autofac.
dotnet add package Autofac.Extensions.DependencyInjection --version 7.2.0
  • Change the ServiceProviderFactory with help of UseServiceProviderFactory.
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Services.AddScoped<IService, ServiceA>();
builder.Services.AddScoped<IService, ServiceB>();
var app = builder.Build();
  • All the service registered works that previously register with default IoC container.
  • Run again and check with same endpoint.
output:
Message from ServiceB.
  • So it works same as earlier. Now it time to take advantage of Autofac and register with Autofac container and utilize functionality of Autofac.

  • Now It's utilizing Autofac.ContainerBuilder.

using Autofac;
using Autofac.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<Autofac.ContainerBuilder>(autofacConfigure =>
{
    autofacConfigure
        .RegisterType<ServiceA>().As<IService>()
        .InstancePerLifetimeScope();
    autofacConfigure
        .RegisterType<ServiceB>().As<IService>()
        .InstancePerLifetimeScope();
});
var app = builder.Build();

Decorator

  • Register Decorator. ( Previously we have to use Scurtor Nuget package ). Here I created sample decorator that just add prefix to message.
public class DecoratorService : IService
{
    private readonly IService service;
    public DecoratorService(IService service)
    {
        this.service = service;
    }

    public string GetMessage()
    {
        return "Received message : " + service.GetMessage();
    }
}
  • Register with Container.
builder.Host.ConfigureContainer<Autofac.ContainerBuilder>(autofacConfigure =>
{
    autofacConfigure
        .RegisterType<ServiceA>().As<IService>()
        .InstancePerLifetimeScope();
    autofacConfigure
        .RegisterType<ServiceB>().As<IService>()       
        .InstancePerLifetimeScope();
    autofacConfigure
        .RegisterDecorator<DecoratorService, IService>();  
});
  • Run again and check with browser.
output:
Received message : Message from ServiceB.

Named Or Keyed To Resolve The Service

  • This is not present in default container with .net core.
  • Let's say you have registered two implementation for same service type. For some cases you want to use one and another case different one based on some context. or let's say you want to resolve both of them and get the result and merge them.
  • In our example, ServiceA and ServiceB is such thing.

Solution 1

  • Autofac provide facility to Named or Keyed to service. Later that used to resolve service.
  • It also needs one implementation that resolve both. ( There is alternative to this and will discuss later in this article.)
  • First let's provide Key to service.
builder.Host.ConfigureContainer<Autofac.ContainerBuilder>(autofacConfigure =>
{
    autofacConfigure
        .RegisterType<ServiceA>().As<IService>()
        .Keyed<IService>("ServiceA")
        .InstancePerLifetimeScope();
    autofacConfigure
        .RegisterType<ServiceB>().As<IService>()
        .Keyed<IService>("ServiceB")
        .InstancePerLifetimeScope();
    autofacConfigure.RegisterType<MergeService>().WithAttributeFiltering();
    autofacConfigure
        .RegisterDecorator<DecoratorService, IService>();  
});
  • Create Merge Service
public class MergeService 
{
    private readonly IService iServiceA;
    private readonly IService iServiceB;

    public MergeService([KeyFilter("ServiceA")] IService iServiceA,
        [KeyFilter("ServiceB")] IService iServiceB)
    {
        this.iServiceA = iServiceA;
        this.iServiceB = iServiceB;
    }

    public IEnumerable<string> GetMessages()
    {
        return new[] { iServiceA.GetMessage(), iServiceB.GetMessage() };
    }
}
  • Note : In above example during registration providing Key as well as MergeService with WithAttributeFiltering() is important.
  • This is how it needs to use.
app.MapGet("/merge", (MergeService service) =>
{
    return service.GetMessages();
}).WithName("Merge");
  • Run the application and hit the browser with http://localhost:5000/merge. Following is the result.
output:
[
"Received message : Message from ServiceA.",
"Received message : Message from ServiceB."
]

Solution 2

  • This is some what in line with Solution 1. I want to highlight that over here that apart from Keyed or Named, there is possibility to provide Metadata and parameter. Here is the same thing we did with Keyed, we can do with Metadata attribute. I am only highlighting changed part.

  • During registration.

autofacConfigure
        .RegisterType<ServiceA>().As<IService>()
        .WithMetadata("ServiceName","ServiceA")
        .InstancePerLifetimeScope();
autofacConfigure
        .RegisterType<ServiceB>().As<IService>()
        .WithMetadata("ServiceName", "ServiceB")
        .InstancePerLifetimeScope();
  • In MergeService use MetadataFilter instead of KeyFilter.
public MergeService([MetadataFilter("ServiceName","ServiceA")] IService iServiceA,
        [MetadataFilter("ServiceName", "ServiceB")] IService iServiceB)
    {
        this.iServiceA = iServiceA;
        this.iServiceB = iServiceB;
    }

Use IContainer of Autofac Directly.

Note: This is not recommended though but just to need to aware that it is available.

Use Autofac.ContainerBuilder directly into Implementation to resolve other dependency. Now this is something like IServiceProvider of default container but Autofac container provide functionality to resolve dependency with named/keyed or metadata without any third service implementation.

  • In above MergeService is good solution and separation of concern but think that we have to do that directly into map handler of merged endpoint.

  • Something like this. ( This is just example so it will not work as of now but later complete solution make it work.)

app.MapGet("/merge", (IResolver resolver) =>
{
    var serviceA = resolver.GetService("ServiceA");
    var serviceB = resolver.GetService("ServiceB");
    return new []{ serviceA.GetMessage() , serviceB.GetMessage() };
}).WithName("Merge");

Let's start.

  • This time registration is bit different.
  • Steps
    • First create ContainerBuilder of Autofac.
    • Register all services over there.
    • Now finally build the container and register as Singleton.
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
var containerBuidler = new Autofac.ContainerBuilder();
containerBuidler
    .RegisterType<ServiceA>().As<IService>()
    .Keyed<IService>("ServiceA")
    .InstancePerLifetimeScope();
containerBuidler
    .RegisterType<ServiceB>().As<IService>()
    .Keyed<IService>("ServiceB")
    .InstancePerLifetimeScope();
containerBuidler.RegisterType<MergeService>().WithAttributeFiltering();
containerBuidler
    .RegisterDecorator<DecoratorService, IService>();
builder.Services.AddSingleton(containerBuidler.Build());

// Default registration
builder.Services.AddScoped<IService, ServiceA>();
var app = builder.Build();
  • From above following will create Autofac container and build that and register as IContainer.
var containerBuidler = new Autofac.ContainerBuilder();
containerBuidler
    .RegisterType<ServiceA>().As<IService>()
    .Keyed<IService>("ServiceA")
    .InstancePerLifetimeScope();
containerBuidler
    .RegisterType<ServiceB>().As<IService>()
    .Keyed<IService>("ServiceB")
    .InstancePerLifetimeScope();
containerBuidler.RegisterType<MergeService>().WithAttributeFiltering();
containerBuidler
    .RegisterDecorator<DecoratorService, IService>();
builder.Services.AddSingleton(containerBuidler.Build());
  • This will be used like this.
app.MapGet("/merge", (Autofac.IContainer container) =>
{
    var serviceA = container.ResolveKeyed<IService>("ServiceA");
    var serviceB = container.ResolveKeyed<IService>("ServiceB");
    return new[] { serviceA.GetMessage(), serviceB.GetMessage() };
}).WithName("Merge");
  • So far so good but now following will not work.
app.MapGet("/hello", (IService service) =>
{    
    return service.GetMessage();
}).WithName("Hello");
  • To make it work needs to register default services. Earlier when we have used ConfigureContainer of builder.Host , at that time internally it will put all service to builder.Services so it is available. Now it is not so we have to default registration.
builder.Services.AddScoped<IService, ServiceA>();
  • If you want to avoid this registration then use following.
app.MapGet("/hello", (IContainer container) =>
{
    var service = container.Resolve<IService>();
    return service.GetMessage();
}).WithName("Hello");
  • Hope above things help. If there is any specific requirement like resolving named or keyed or metadata based service, decorator usage then it is worth using Autofac. Most of the time default container will there for most of time and this will help in corner cases and some advanced functionality.
  • Also named service or metadata service is not like out of reach for default IoC implementation. If you carefully create factory then it will work but resolve of that happen using factory. This point I will cover in separate blog post as for this article, wanted to scope within autofac mainly.