Autofac in ASP.NET Core 3 – advanced stuff

data: 29 grudnia, 2020
czas czytania: 6 min
autor: Tymoteusz Dragon

Dependency Injection, one of Inversion of Control techniques, has been a well-known approach for many years now. However, it took a while to easily use it in applications built on top of the ASP.NET, for instance, it was almost not possible to use it in ASP.NET WebForms application.

Some Service Locator-based solutions could be used, but it was far from perfect. With ASP.NET MVC, we were given a possibility to set our own Dependency Resolver – an external library to handle dependency injection. Finally, with the introduction of ASP.NET Core, Microsoft has provided a built-in basic dependency injection container, and using it has become a common practice for .NET Core web application programming. On the other hand, it does not mean that alternative libraries cannot be used, when more advanced features are needed. Looking into NuGet, we can see the popularity of libraries like Autofac or Unity is still rising.

Using Autofac in ASP.NET Core 3 applications

First thing to do is adding a reference to Autofac.Extensions.DependencyInjection from NuGet. Then, Autofac has to be set up as a service provider, when configuring a host in CreateHostBuilder method of Program class:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Now, dependencies can be configured in Startup class. By convention, void ConfigureContainer(ContainerBuilder builder) method is a place, where all registration are made. It does not mean that ConfigureServices is no longer needed; on the contrary – all services registered there would be visible in Autofac container. That means, all external library dependencies, which expose IServiceCollection extension methods, should be used as usually.

“Lazy-loaded” dependencies

There are scenarios when initialization of a dependency may be postponed until it is really needed. It might be required if:

  • Dependency creation is time- or resource-consuming;
  • The application flow does not need the dependency due to i.e. validation error.

In such a case, the dependency may be wrapped in a Lazy<T> object. Such a dependency remains unresolved until the Value property of the Lazy object is accessed. It is also possible to verify whether it’s already resolved with IsValueCreated property.

Assuming there is the WeatherForecastController and the WeatherForecastService implementing IWeatherForecastService interface registered in the container, instead of a direct constructor injection like this:

public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherForecastService _weatherForecastService;
 
    public WeatherForecastController(IWeatherForecastService weatherForecastService)
    {
        _weatherForecastService = weatherForecastService;
    }
 
    [HttpGet]
    public IActionResult Get()
    {
        if (!Validate()) // or some other code or validation
        {
            return BadRequest();
        }
 
        return Ok(_weatherForecastService.Forecast());
    }
    // ...
}

IWeatherForecastService could be wrapped with a Lazy<T> object:

public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly Lazy<IWeatherForecastService> _weatherForecastService;
 
    public WeatherForecastController(Lazy<IWeatherForecastService> weatherForecastService)
    {
        _weatherForecastService = weatherForecastService;
    }
 
    [HttpGet]
    public IActionResult Get()
    {
        if (!Validate()) // or some other code or validation
        {
            return BadRequest();
        }
 
        return Ok(_weatherForecastService.Value.Forecast());
    }
    // ...
}

In such a case, weatherForecastService would remain uninitialized until Value property is called. If validation fails and the method returns BadRequest, weatherForecastService would not be initialized at all.

As the Lazy dependency initialization is an out-of-the-box feature of Autofac, no additional steps are required during the registration process in order to make use of it. In addition, a Lazy class comes from .NET Core itself (as a part of System namespace), so there is no tight coupling to the container.
The lazy dependency initialization feature is especially useful in controllers which implement multiple different endpoints, each with its separate dependencies.

Custom runtime parameters

Another, more complex scenario, is passing additional parameters to the injected dependencies. It is a common practice to use constructor injections, with all parameters automatically resolved by the container. However, if one of the parameters, for instance a primitive type one, cannot be resolved, an Autofac runtime exception will be raised. There are two solutions to this problem:

  • Func delegate family,
  • Custom delegate.

Func delegate

Actually, there are three different usages of the Func delegates:

  • To postpone dependency resolution,
  • To create multiple instances of the dependency,
  • To pass custom parameter to the dependency.

Whenever Func<TOut> (where TOut defines a dependency to use) is injected, it doesn’t undergo immediate resolution, similarly to Lazy<T> dependencies described earlier. If the dependency is set as transient (instance per dependency), each time we call the Func delegate, a new instance of the injected service is created.
However, a Func delegate can be injected with more than one generic parameter, with additional parameters defining delegate constructor parameters. To illustrate it, let’s assume that the aforementioned WeatherForecastService (which implements IWeatherForecastService) has a non-default constructor of the following form:

public WeatherForecastService(string country, ILogger<WeatherForecastService> logger)

So, to create a dependency, a custom country code has to be provided, as well as an container-side injected logger service. For WeatherForecastController to be able to inject an implementation of the service, its dependency has to be declared as follows:

public WeatherForecastController(Func<string, IWeatherForecastService> weatherForecastService)

From now on, the delegate can be simply called as follows:

weatherForecastService("USA")

Such a construct makes it possible so that a hard-coded parameter (a country code) co-exists with container-side dependency injections managed by Autofac.

However, a new issue reveals itself as soon as two custom parameters of the same CLI type are required. The downside of using Func is that it does its parameter matching by type. It means, when we have e.g. 2 custom string parameters, Autofac is unable to differentiate them and thus throws an exception. To solve this problem, a custom delegate needs to be used.

Custom delegate

With the requirements changed so that in addition to a country code a state code has to be provided, both being of string type, the constructor of the IWeatherForecastService would look like:

public WeatherForecastService(string country, string state, ILogger<WeatherForecastService> logger)

This issue can be resolved by creating a delegate:

public delegate IWeatherForecastService WeatherForecastServiceFactory(string country, string state);

and then called to instantiate the service:

weatherForecastService("USA", "California")

Neither approach requires changes in container configuration. Autofac can automatically use both Func and custom delegates.
Summary and then, instead of injecting the service either directly or as a Func object, this delegate can be injected:

Autofac, as a standard dependency injection solution in .NET Core, is a welcome addition. While the support for the already built-in framework and the container themselves is often sufficient, there are more sophisticated scenarios which require a more flexible solution.

Sources:

Newsletter

Zainteresowały Cię nasze treści?
Sprawdź co jeszcze przygotowaliśmy.

Adres e-mail

Dziękujemy! Na Twój adres e-mail wysłaliśmy prośbę o potwierdzenie zapisu do newslettera.

O nie! Coś poszło nie tak. Nie zapisałeś się.

Gdyby tylko dało się zapisać Twojego maila dwa razy :)

Niepoprawny mail. Spróbuj jeszcze raz.

Cookies

W pracy serwujemy suchar dnia. Tutaj musimy Cię poczęstować ciasteczkami. Dowiedz się więcej.