EF Core 1.1

Implementing a provider 'Use...' method for EF Core 1.1

The previous post contained lots of information about how dependency injection works with database providers. This post adds more to the provider story by explaining how to implement a method like UseSqlServer that allows applications to select the provider to use.

IDatabaseProvider

The previous post showed how database providers implement IDatabaseProviderServices to define provider-specific services. The post also covered how services are registered with an "Add..." method. One of the services registered must be for IDatabaseProvider. This service is registered using TryAddEnumerable, which means that if three providers have been registered, then there will be three registrations for IDatabaseProvider in the container.

The context decides which provider is in use by calling IsConfigured on each registered IDatabaseProvider, passing in the current DbContextOptions. (Note that an exception is thrown if anything other than exactly one IDatabaseProvider returns true for IsConfigured. This means an exception is thrown if an application makes calls to multiple "Use.." for the same context instance.)

Once an IDatabaseProvider has been selected, its GetProviderServices method is called to get the IDatabaseProviderServices for the provider.

The result of all this is that IDatabaseProvider is the mechanism by which a provider is chosen from the current DbContextOptions.

IDbContextOptionsExtension

Options extensions are the mechanism whereby providers can add information to the DbContextOptions for the context. An options extension is registered by the 'Use...' method, as described below. Each provider's extension implements the IDbContextOptionsExtension. EF will then call the ApplyServices method to register provider services in the D.I. container.

For example, a trimmed down version of the in-memory provider's options extension looks something like this:

public class InMemoryOptionsExtension : IDbContextOptionsExtension
{
    public InMemoryOptionsExtension()
    {
    }

    public InMemoryOptionsExtension([NotNull] InMemoryOptionsExtension copyFrom)
    {
        StoreName = copyFrom.StoreName;
    }

    public virtual string StoreName { get; set; }

    public virtual void ApplyServices(IServiceCollection services)
        => services.AddEntityFrameworkInMemoryDatabase();
}

Notice that ApplyServices simply calls the AddEntityFrameworkInMemoryDatabase that was described in the previous post.

The options extension also holds additional information needed by the provider. For example, relational options extensions will contain the connection string to use. The in-memory extension shown above holds the name of the in-memory database to use.

Implementing the 'Use...' method

A trimmed-down version of the UseInMemoryDatabase method looks something like this:

public static DbContextOptionsBuilder UseInMemoryDatabase(
    this DbContextOptionsBuilder optionsBuilder,
    string databaseName)
{
    var extension = optionsBuilder.Options.FindExtension<InMemoryOptionsExtension>();

    extension = extension != null
        ? new InMemoryOptionsExtension(extension)
        : new InMemoryOptionsExtension();

    extension.StoreName = databaseName;

    ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

    return optionsBuilder;
}

Walking through this code:

Notice that after this call, the options will have an InMemoryOptionsExtension registered.

Coupling IDatabaseProvider and IDbContextOptionsExtension

As discussed above, the IsConfigured method of IDatabaseProvider must return true if the provider is being used. The previous section showed that calling a 'Use...' method will result in an options extension being added to the options. Therefore, IsConfigured should return true if an options extension for the provider has been added to the options.

The DatabaseProvider class implements IDatabaseProvider to do this:

public class DatabaseProvider<TProviderServices, TOptionsExtension> : IDatabaseProvider
    where TProviderServices : class, IDatabaseProviderServices
    where TOptionsExtension : class, IDbContextOptionsExtension
{
    public virtual IDatabaseProviderServices GetProviderServices(IServiceProvider serviceProvider)
        => serviceProvider.GetRequiredService<TProviderServices>();

    public virtual bool IsConfigured(IDbContextOptions options)
        => options.Extensions.OfType<TOptionsExtension>().Any();
}

This means that a provider should register a DatabaseProvider class to couple provider selection with the 'Use...' method. For example:

services.TryAddEnumerable(ServiceDescriptor
    .Singleton<IDatabaseProvider, DatabaseProvider<InMemoryDatabaseProviderServices, InMemoryOptionsExtension>>());

When an InMemoryOptionsExtension is registered, the InMemoryDatabaseProviderServices will be selected.

Additional 'Use...' method details

The generic overload

A provider should implement an overload of its 'Use...' method that works with the generic DbContextOptionsBuilder. For example:

public static DbContextOptionsBuilder<TContext> UseInMemoryDatabase<TContext>(
    this DbContextOptionsBuilder<TContext> optionsBuilder,
    string databaseName)
    where TContext : DbContext
    => (DbContextOptionsBuilder<TContext>)UseInMemoryDatabase(
        (DbContextOptionsBuilder)optionsBuilder, databaseName);

This ensures that any chained methods will still get the generic DbContextOptionsBuilder.

Nested provider-specific configuration

Configuration fundamental to the use of the provider should be passed as arguments to the 'Use...' method. For example, the connection string. Other provider-specific configuration can be done in a nested closure. For example:

optionsBuilder.UseSqlServer(
    _connectionString,
    b => b.CommandTimeout(10));

Here, CommandTimeout is defined on SqlServerDbContextOptionsBuilder. The relevant parts of UseSqlServer look something like this:

public static DbContextOptionsBuilder UseSqlServer(
    this DbContextOptionsBuilder optionsBuilder,
    string connectionString,
    Action<SqlServerDbContextOptionsBuilder> sqlServerOptionsAction = null)
{
    // Usual options extension stuff...

    sqlServerOptionsAction?.Invoke(new SqlServerDbContextOptionsBuilder(optionsBuilder));

    return optionsBuilder;
}

Notice that the delegate has a default of null since often code does not need to set any additional options. The CommandTimeout method looks very similar to the 'Use...' method:

public virtual SqlServerDbContextOptionsBuilder CommandTimeout(int? commandTimeout)
{
    var extension = new SqlServerOptionsExtension(
        OptionsBuilder.Options.GetExtension<SqlServerOptionsExtension>());

    extension.CommandTimeout = commandTimeout;

    ((IDbContextOptionsBuilderInfrastructure)OptionsBuilder).AddOrUpdateExtension(extension);

    return this;
}

This method works just as is described above for the 'Use...' method:

Summary

The "Use.." method configures a provider for use by adding an options extension to the DbContextOptions. The context uses this in provider selection to determine which IDatabaseProviderServices to return.


This page is up-to-date as of November 3rd, 2016. Some things change. Some things stay the same. Use your noggin.