Access services inside ConfigureServices using IConfigureOptions in ASP.NET Core

Datetime:2016-08-22 22:08:48          Topic: ASP.NET           Share

In arecent post I showed how you could populate an IOptions<T> object from the database for the purposes of caching the query result. It wasn't the most flexible solution or really recommended but it illustrated the point.

However one of the issues I had with the solution was the need to access configured services from within the IOptions<T> configuration lambda, inside ConfigureServices itself.

The solution I came up with was to use the injected IServiceCollection to build an ISerivceProvider to get the configured service I needed. As I pointed out at the time, this serivce-locator pattern felt icky and wrong, but I couldn't see any other way of doing it.

Thankfully, and inspired by this post from Ben Collins , there is a much better solution to be had by utilising the IConfigureOptions<T> interface

The previous version

In my post, I had this (abbreviated) code, which was trying to access an Entity Framework Core DbContext in the Configure method to setup the MultitenancyOptions class:

public class Startup  
{
    public Startup(IHostingEnvironment env) { /* ... build configuration */ }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // add MVC, connection string etc

        services.Configure<MultitenancyOptions>(  
            options =>
            {
                var scopeFactory = services
                    .BuildServiceProvider()
                    .GetRequiredService<IServiceScopeFactory>();

                using (var scope = scopeFactory.CreateScope())
                {
                    var provider = scope.ServiceProvider;
                    using (var dbContext = provider.GetRequiredService<ApplicationDbContext>())
                    {
                        options.AppTenants = dbContext.AppTenants.ToList();
                    }
                }
            });

        // add other services
    }

    public void Configure(IApplicationBuilder app) { /* ... configure pipeline */ }
}

Yuk. As you can see, the call to Configure is a mess. In order to obtain a scoped lifetime DbContext it has to build the service collection to produce an IServiceProvider , to then obtain an IServiceScopeFactory . From there it can create the correct scoping, create another IServiceProvider , and finally find the DbContext we actually need. This lambda has way too much going on, and 90% of it is plumbing.

If you're wondering why you shouldn't just fetch a DbContext directly from the first service provider, check out this twitter discussion between Julie Lerman, David Fowler and Shawn Wildermuth.

The new improved answer

So, now we know what we're working with, how do we improve it? Luckily, the ASP.NET team anticipated this issue - instead of providing a lambda for configuring the MultitenancyOptions object, we implement the IConfigureOptions<TOptions> interface, where TOptions: MultitenancyOptions . This interface has a single method, Configure , which is passed a constructed MultitenancyOptions object for you to update:

public class ConfigureMultitenancyOptions : IConfigureOptions<MultitenancyOptions>  
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    public ConfigureMultitenancyOptions(IServiceScopeFactory serivceScopeFactory)
    {
        _serviceScopeFactory = serivceScopeFactory;
    }

    public void Configure(MultitenancyOptions options)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var provider = scope.ServiceProvider;
            using (var dbContext = provider.GetRequiredService<ApplicationDbContext>())
            {
                options.AppTenants = dbContext.AppTenants.ToList();
            }
        }
    }
}

We then just need to register our configuration class in the normal ConfigureServices method, which becomes:

public void ConfigureServices(IServiceCollection services)  
{
    // add MVC, connection string etc

    services.AddSingleton<IConfigureOptions<MultitenancyOptions>, ConfigureMultitenancyOptions>();

    // add other services
}

The advantage of this approach is that the configuration class is created through the usual DI container, so can have dependencies injected simply through the constructor. There is still a slight complexity introduced by the fact we want MultitenancyOptions to have a singleton lifecycle. To prevent leaking a lifetime scope, we must inject an IServiceScopeFactory and create an explicit scope before retrieving our DbContext . Again, check out Julie Lerman's twitter conversation and associated post for more details on this.

The most important point here is that we are no longer calling BuildServiceProvider() in our Configure method, just to get a service we need. So just try and forget that I ever mentioned doing that ;)

Under the hood

In hindsight, I really should have guessed that this approach was possible, as the lambda approach is really just a specialised version of the IConfigureOptions approach.

Taking a look at the Options source code really shows how these two methods tie together. The Configure extension method on IServiceCollection that takes a lambda looks like the following (with precondition checks etc removed)

public static IServiceCollection Configure<TOptions>(  
    this IServiceCollection services, Action<TOptions> configureOptions)
{
    services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureOptions<TOptions>(configureOptions));
    return services;
}

All this method is doing is creating an instance of the ConfigureOptions<TOptions> class, passing in the configuration lambda, and registering that as a singleton. That looks suspiciously like our tidied up approach, the difference being that we left the instantiation of our ConfigureMultitenancyOptions to the DI system, instead of new-ing it up directly.

As is to be expected, the ConfigureOptions<TOptions> , which implements IConfigureOptions<TOptions> just calls the provided lambda in it's Configure method:

public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class  
{
    public ConfigureOptions(Action<TOptions> action)
    {
        Action = action;
    }

    public Action<TOptions> Action { get; }

    public virtual void Configure(TOptions options)
    {
        Action.Invoke(options);
    }
}

So again, the only substantive difference between using the lambda approach and the IConfigureOptions approach is that the latter allows you to inject services into your options class to be used during configuration.

One final useful point to be ware of: you can register multiple instances of IConfigureOptions<TOptions> for the same TOptions . They will all be applied, and in the order they were added to the service collection in ConfigureServices . That allows you to do simple configuration in ConfigureServices using the Configure lambda, while using a separate implementation of IConfigureOptions elsewhere, if you're so inclined.





About List