EF Core 1.1

EF Core Dependency Injection Internals

A previous post gave an overview of how dependency injection is used internally by EF Core, and how applications might interact with this. In this post we will look at some of the internal details. This post is aimed at provider writers and people who may want to contribute to the EF source code. Application developers should not need to know any of this.

Principles

Two principles are used throughout EF's internal services:

When used together these principles make it easy to see the dependencies in the system. It ensures that services only make use of the public API service defined by other services and do not depend on any implementation details.

(There are some cases where constructor injection is not possible, usually when dependencies are conditional. For example, the QueryContextFactory depends on IStateManager for tracking entities. However, if the query is a no-tracking query, then the state manager is not needed. Therefore, as a perf optimization, the QueryContextFactory does not depend on IStateManager directly, but instead depends on ICurrentDbContext. This in turns allows IStateManager to be loaded if it is needed by using ICurrentDbContext as a service locator.)

Scopes

EF Core creates a scope for each context instance such that there is essentially a scope for each session. This scope has two purposes:

Service registration and lifetimes

Registration of core EF services is done in an IServiceCollection extension method called AddEntityFramework. A very trimmed down version of this method looks like this:

public static IServiceCollection AddEntityFramework(
    [NotNull] this IServiceCollection serviceCollection)
{
    serviceCollection.TryAddEnumerable(new ServiceCollection()
        .AddScoped<IEntityStateListener, INavigationFixer>(p => p.GetService<INavigationFixer>())
        .AddScoped<INavigationListener, INavigationFixer>(p => p.GetService<INavigationFixer>())
        .AddScoped<IEntityStateListener, ILocalViewListener>(p => p.GetService<ILocalViewListener>()));

    serviceCollection.TryAdd(new ServiceCollection()
        .AddSingleton<IDbSetFinder, DbSetFinder>()
        .AddScoped<INavigationFixer, NavigationFixer>()
        .AddScoped<ValueGeneratorSelector>()
        .AddScoped<IModel>(p => p.GetRequiredService<IDbContextServices>().Model)
        .AddScoped<IValueGeneratorSelector>(p => p.GetRequiredService<IDbContextServices>().DatabaseProviderServices.ValueGeneratorSelector)
        .AddScoped<IValueGeneratorCache>(p => p.GetRequiredService<IDbContextServices>().DatabaseProviderServices.ValueGeneratorCache));

    return serviceCollection;
}

Most of the service registrations have been removed to leave just enough to explain the concepts going on here:

Database provider services

Database providers interact with EF Core by implementing various services. These services are registered in D.I., as shown later, and are then used through constructor injection in the normal way. Multiple providers can register their services in the same D.I. container. The context is then responsible for determining which provider is in use for the current session and ensuring that the services for that provider are resolved from D.I.

For example, see the registration for IValueGeneratorSelector in the code above. This service is resolved as follows:

Registering provider services

Database providers should ship with a method like AddEntityFramework. For example, the in-memory provider has a method called AddEntityFrameworkInMemoryDatabase. A cut-down version of this method looks like this:

public static IServiceCollection AddEntityFrameworkInMemoryDatabase(
    [NotNull] this IServiceCollection services)
{
    services.AddEntityFramework();

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

    services.TryAdd(new ServiceCollection()
        .AddSingleton<InMemoryValueGeneratorCache>()
        .AddSingleton<IInMemoryTableFactory, InMemoryTableFactory>()
        .AddScoped<InMemoryValueGeneratorSelector>());

    return services;
}

Things to notice:

IDatabaseProviderServices implementation

The IDatabaseProviderServices implementation ties all this together. Here is a cut-down version of the in-memory implementation:

public class InMemoryDatabaseProviderServices : DatabaseProviderServices
{
    public InMemoryDatabaseProviderServices([NotNull] IServiceProvider services)
        : base(services)
    {
    }

    public override IValueGeneratorSelector ValueGeneratorSelector 
        => GetService<InMemoryValueGeneratorSelector>();

    public override IValueGeneratorCache ValueGeneratorCache 
        => GetService<InMemoryValueGeneratorCache>();
}

The ValueGeneratorSelector property is overridden to call GetService for InMemoryValueGeneratorSelector. This completes the story of how IValueGeneratorSelector is resolved end-to-end:

Also notice that InMemoryDatabaseProviderServices extends from DatabaseProviderServices. This class provides implementations for some services. For example, a basic ValueGeneratorSelector implementation was registered in AddEntityFramework. This is returned by the ValueGeneratorSelector property of the DatabaseProviderServices base class. So if a provider doesn't need to provide its own implementation, then it doesn't need to register anything or override anything and it will get the basic implementation shipped with EF.

Registering concrete instances

At the very top of this post it was stated that all EF services are defined by interfaces. Why, then, are some concrete instances registered in D.I.? The answer is that when EF code resolves the interface for a provider service that service is obtained from D.I. by a call to GetService for the concrete implementation. For example, ValueGeneratorSelector or InMemoryValueGeneratorSelector, which are resolved by calls to GetService in DatabaseProviderServices or InMemoryDatabaseProviderServices respectively. Other services should not depend on the concrete implementations.

Singleton verses scoped provider services

All provider services are registered as scoped. This is because a different instance may be returned depending on which provider is being used for the current session. However, some provider services must also act as a cache root, which means that the service must be registered as a singleton. This is done by registering the concrete implementation as a singleton even though the service interface is registered as scoped.

For example, InMemoryValueGeneratorCache is registered as a singleton. This means there will be only one of these caches for all context instances. However, IValueGeneratorCache is registered in AddEntityFramework as scoped. This means that for any session where the in-memory provider is in use, the singleton InMemoryValueGeneratorCache will be used. But if a different provider is in use, then resolution will go through a different IDatabaseProviderServices, which will return a different singleton.

Relational providers

All of the examples above used the in-memory provider as an example. Relational providers are exactly the same except that there is an interface IRelationalDatabaseProviderServices that extends from IDatabaseProviderServices and adds relational-specific services. There is a base class implementation of this which the provider implementation should extend. Finally, there in an AddEntityFrameworkRelational method that should be called in the provider's 'Add...' method to ensure relational services are registered in addition to core services.

Summary

The internal use of D.I. by EF Core makes use of a variety of mechanisms to ensure that services can depend on other services in a natural way while still allowing services to be resolved in special ways. EF creates a new service scope per session and some services are resolved dynamically within that scope. This allows the correct provider services to be resolved depending on the provider in use for the current session.

Future posts will cover the other things providers need to do to get all their services working together with EF Core.


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