ASP.NET Core, C#, Programming

Tips on using Autofac in .NET Core 3.x

.NET Core supports DI (dependency injection) design pattern which is technique for achieving Inversion of Control (IoC) between classes and their dependencies. The native, minimalistic implementation is known as conforming container and it is anti-pattern. You can read more about issues related with it here. There is a promise from Microsoft to make better integration points with 3rd party DI vendors in a new .NET Core releases but it is good enough for most of the pet projects and small production projects. Being a user of DI for quite a long time I used to have more broad support from dependency injection frameworks. For the sake of keeping this article short and focused I would skip the definite list of functionality I miss in native DI implementation for .NET Core. I will mention only few: extended lifetime scopes support, automatic assembly scanning for implementations, aggregate services and multi-tenant support. There plenty of DI frameworks on the market. Back in 2008/2009 when I switched to .NET one of my favorite DI frameworks was StructureMap. Having rich functionality it was one of the standard choice for my projects. Another popular framework was Castle Windsor. For some time I was also a user of a Ninject DI which I found very easy to use.

However, StructureMap was deprecated for some time already, Ninject is still good, but I was looking for some different DI to try with one of my new .NET Core projects. Autofac caught my attention immediately. It is on the market since 2007 and it gets only better with 3400+ stars and 700+ forks on the GitHub. It has exhaustive documentation and feature list. On the moment of writing this post the latest version of Autofac is 6 and the way how you bootstrap it in .NET Core 3.x and 5 changed compared to 5.x branch.

So, enough talks: talk is cheap, show me some code…

Tip 1

Setting-up

public class Program
{
  public static void Main(string[] args)
  {
    // ASP.NET Core 3.0+:
    // The UseServiceProviderFactory call attaches the
    // Autofac provider to the generic hosting mechanism.
    var host = Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webHostBuilder => {
          webHostBuilder
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>();
        })
        .Build();
    host.Run();
  }
}

Startup Class

public class Startup
{
  public Startup(IHostingEnvironment env)
  {
    // In ASP.NET Core 3.0 `env` will be an IWebHostEnvironment, not IHostingEnvironment.
    this.Configuration = new ConfigurationBuilder().Build();
  }
  public IConfigurationRoot Configuration { get; private set; }
  public ILifetimeScope AutofacContainer { get; private set; }
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddOptions();
  }

  public void ConfigureContainer(ContainerBuilder builder)
  {
    // Register your own things directly with Autofac here. Don't
    // call builder.Populate(), that happens in AutofacServiceProviderFactory
    // for you.
    builder.RegisterModule(new MyApplicationModule());
  }

  public void Configure(
    IApplicationBuilder app,
    ILoggerFactory loggerFactory)
  {
    // If, for some reason, you need a reference to the built container, you
    // can use the convenience extension method GetAutofacRoot.
    this.AutofacContainer = app.ApplicationServices.GetAutofacRoot();
    loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();
    app.UseMvc();
  }
}

Tip 2

Scanning assemblies

Autofac can use conventions to find and register components in assemblies.

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterAssemblyTypes(typeof(Startup).Assembly)
        .AsClosedTypesOf(typeof(IConfigureOptions<>));
 }

This will register types that are assignable to closed implementations of the open generic type. In that case it will register all implementations of IConfigureOptions<>. See options pattern for more information on how to configure configuration settings with dependency injection.

Tip 3

Use Mvc/Api controllers instantiation with Autofac

Controllers aren’t resolved from the container; just controller constructor parameters. That means controller lifecycles, property injection, and other things aren’t managed by Autofac – they’re managed by ASP.NET Core. You can change that using AddControllersAsServices().

  public void ConfigureServices(IServiceCollection services)
  {
    services.AddControllersAsServices();
  }
public void ConfigureContainer(ContainerBuilder builder) {
	
	var controllersTypesInAssembly = typeof(Startup).Assembly.GetExportedTypes().Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToArray();
	builder.RegisterTypes(controllersTypesInAssembly).PropertiesAutowired();
}

Here we register all types that are descendants of ControllerBase type. We also enable property injection capability (line 5). This is useful when you want to have some property in the base controller implementation which could be re-used (e.g. IMediator).

Tip 4

Register EF Core DbContext with Autofac

If you use Entity Framework Core you want your DbContext to be managed by DI container. One important notice is that DbContext should behave as a unit of work and be scoped to request lifetime. In native DI it registered as a scoped service which in Autofac equal to InstancePerLifetimeScope.

public static void AddCustomDbContext(this ContainerBuilder builder, IConfiguration configuration) {
	builder.Register(c => {
		var options = new DbContextOptionsBuilder<ApplicationContext>();
		options.UseLoggerFactory(c.Resolve<ILoggerFactory>()).EnableSensitiveDataLogging();
		options.UseSqlServer(configuration["ConnectionStrings:ApplicationDb"], sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
			sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
		});
		return options.Options;
	}).InstancePerLifetimeScope();
	builder.RegisterType<ApplicationContext>()
          .AsSelf()
          .InstancePerLifetimeScope();
}
public void ConfigureContainer(ContainerBuilder builder) {
	
	builder.AddCustomDbContext(this.Configuration);
}

Tip 5

Use modules for your registrations

public void ConfigureContainer(ContainerBuilder builder) {
	
	builder.RegisterModule(new MediatorModule());
	builder.RegisterModule(new ApplicationModule());
}
public class ApplicationModule: Autofac.Module {
	public ApplicationModule() {}
	protected override void Load(ContainerBuilder builder) {
		builder.RegisterType<NinjaService>().As<INinjaService>().SingleInstance();
		builder.RegisterType<KatanaService>().As<IKatanaService>().InstancePerLifetimeScope();
	
		builder.RegisterAssemblyTypes(typeof(Startup).GetTypeInfo().Assembly).AsClosedTypesOf(typeof(INinjaRepository<>)).InstancePerLifetimeScope();
	}
}

Keeping registrations in modules makes your wire-up code structured and allow deployment-time settings to be injected.

Tip 6

Follow best practices and recommendations

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s