Entity Framework Core Extension tips & tricks - Injecting dependencies into EF Interceptors

Entity Framework Core Extension tips & tricks - Injecting dependencies into EF Interceptors

Entity Framework interceptors are a great way to make the framework execute your code before (or after) it does its' magic. If you're not familiar with them, read the documentation at Interceptors.

With the just-released Entity Framework 7, it's great to see the EF team's commitment to interceptors and lifecycle hooks. Just look at the long list of new possibilities in the new release of the library:

Events and interception (aka lifecycle hooks) · Issue #626 · dotnet/efcore
Done in 2.1 Add events for entity state changes #10895 Done in 3.1 Add IDbCommandInterceptor or similar to allow injecting custom query execution service #15066 IDbConnectionInterceptor and IDbTran...

The documentation probably answers most questions you might have, but there is one thing missing (at least, as of writing this post) - injecting dependencies into them. While some of them might be constructed by simply calling new or using a factory either in the constructor or inside the AddInterceptors call, sometimes that might just not cut it.

But fret not - not all is lost yet. There are a couple of ways to resolve the dependencies from a DI container and use them in your interceptors.

All the examples below will focus on a specific case where the interceptor UserContextInterceptor is part of an external library and depends on an IUserProvider that should be implemented by the library user. The catch is that the code that adds the interceptor is located in the library. Therefore the library does now know the dependencies of the IUserProvider and needs to resolve it from the DI container.

This might seem very specific - but it is one of the most complicated cases which might be easily adapted to simpler ones.

First way

This one is definitely not recommended, but for the sake of completeness, it'll be included here. Here's the DbContextOptionsBuilder extension:

public static DbContextOptionsBuilder AddInterceptor<TUserProvider>(this DbContextOptionsBuilder optionsBuilder, IServiceCollection services) where TUserProvider : class, IUserProvider
{
    services.AddScoped<IUserProvider, TUserProvider>();
    var provider = services.BuildServiceProvider();
    var userProvider = provider.GetRequiredService<IUserProvider>();
    return optionsBuilder.AddInterceptors(new UserContextInterceptor(userProvider));
}

And here's how it's used:

Host.CreateDefaultBuilder(args)
    .ConfigureServices(
        services =>
        {
            services.AddDbContext<ApplicationDbContext>(
                options =>
                {
                    options.UseSqlServer("<connection-string>")
                           .AddInterceptor<UserProvider>(services);
                });
        });

The upside is that both the IUserProvider service and interceptor registration are wrapped in just one call, so the library user does not need to register anything else. The downside is calling the IServiceCollection.BuildServiceProvider method manually each time a DbContext is instantiated.

Second way

The second approach will require the user to add two method calls to their host configuration.

public static IServiceCollection AddUserProvider<TUserProvider>(this IServiceCollection services) where TUserProvider : class, IUserProvider 
    => services.AddScoped<IUserProvider, TUserProvider>();

public static DbContextOptionsBuilder AddInterceptor(this DbContextOptionsBuilder optionsBuilder, IServiceProvider provider)
{
    var userProvider = provider.GetRequiredService<IUserProvider>();
    return optionsBuilder.AddInterceptors(new UserContextInterceptor(userProvider));
}
Host.CreateDefaultBuilder(args)
    .ConfigureServices(
        services =>
        {
            services.AddUserProvider<UserProvider>();
            services.AddDbContext<ApplicationDbContext>(
                (provider, options) =>
                {
                    options.UseSqlServer("<connection-string>")
                            .AddInterceptor(provider);
                });
        });

Note the slightly different AddDbContext overload. This is better because we are not forced to build the service provider each time on our own. However, the user must remember to add the AddUserProvider call.

Third way - see update

This way will produce the cleanest piece of code to be used by the application, but there is a bit more code to be written. If you're just planning on adding a simple interceptor, then this might be overkill, and the previous approach might be better.

If you look at the IInterceptor interface source, you will see this handy comment:

/// <summary>
///     The base interface for all Entity Framework interceptors.
/// </summary>
/// <remarks>
///     <para>
///         Interceptors can be used to view, change, or suppress operations taken by Entity Framework.
///         See the specific implementations of this interface for details. For example, 'IDbCommandInterceptor'.
///     </para>
///     <para>
///         Use <see cref="DbContextOptionsBuilder.AddInterceptors(Microsoft.EntityFrameworkCore.Diagnostics.IInterceptor[])" />
///         to register application interceptors.
///     </para>
///     <para>
///         Extensions can also register multiple <see cref="IInterceptor" />s in the internal service provider.
///         If both injected and application interceptors are found, then the injected interceptors are run in the
///         order that they are resolved from the service provider, and then the application interceptors are run
///         in the order that they were added to the context.
///     </para>
///     <para>
///         See <see href="https://aka.ms/efcore-docs-interceptors">EF Core interceptors</see> for more information and examples.
///     </para>
/// </remarks>
public interface IInterceptor
{
}
source

Lines #14 - #18 are the clue here. This means that an interceptor might be simply registered as IInterceptor in an extension's internal service provider and will be picked up by Entity Framework automatically.

But what's the extension thingy? It's an implementation of IDbContextOptionsExtension, which gives the authors of EF providers (or simpler  extensions) the ability to register services they need for their code to work.

First, you need to create two classes - an IDbContextOptionsExtension implementation and DbContextOptionsExtensionInfo "child" class:

public class EfCoreOptionsExtension : IDbContextOptionsExtension
{
    private readonly Action<IServiceCollection> _addServices;

    public DbContextOptionsExtensionInfo Info { get; }

    public EfCoreOptionsExtension(Action<IServiceCollection> addServices)
    {
        _addServices = addServices;
        Info = new EfCoreOptionsExtensionInfo(this);
    }

    public void ApplyServices(IServiceCollection services) => _addServices(services);

    public void Validate(IDbContextOptions options)
    { }
}

public class EfCoreOptionsExtensionInfo : DbContextOptionsExtensionInfo
{
    public override bool IsDatabaseProvider => false;

    public override string LogFragment => "EfCoreOptionsExtension";

    public EfCoreOptionsExtensionInfo(IDbContextOptionsExtension extension) : base(extension)
    { }

    public override int GetServiceProviderHashCode() => 0;

    public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => string.Equals(LogFragment, other.LogFragment, StringComparison.Ordinal);

    public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
    { }
}

Now, the DbContextOptionsBuilder extension should look like this:

public static DbContextOptionsBuilder AddInterceptor<TUserProvider>(this DbContextOptionsBuilder optionsBuilder, IServiceProvider provider) where TUserProvider : class, IUserProvider
{
    ((IDbContextOptionsBuilderInfrastructure)optionsBuilder)
        .AddOrUpdateExtension(
            new EfCoreOptionsExtension(
                services =>
                {
                    services.AddScoped<IUserProvider, TUserProvider>();
                    services.AddScoped<IInterceptor, UserContextInterceptor>();
                }));

    return optionsBuilder;
}

And the user will just have to call:

Host.CreateDefaultBuilder(args)
    .ConfigureServices(
        services =>
        {
            services.AddDbContext<ApplicationDbContext>(
                options =>
                {
                    options.UseSqlServer("<connection-string>")
                           .AddInterceptor<UserProvider>();
                });
        });

This combines the best of both worlds - just one call for the library user to make and no manual IServiceProvider building.

There are plans to let users do the same thing but without all the IDbContextOptionsExtension hassle:

Ability to register IInterceptor without an IDbContextOptionsExtension · Issue #21578 · dotnet/efcore
I want to use Azure Managed Identities to connect to an Azure SQL instance. I&#39;m trying to use a DbConnectionInterceptor to handle the ConnectionOpeningAsync &quot;event&quot; to use the Azure.I...

UPDATE 28.04.2023:

Unfortunately, after some time and coming upon this problem again, I noticed that the last method (which I liked the most at the time) has a critical issue associated with it.

EF Core is designed to keep its services separate from your application's IServiceProvider. For most cases, this design choice is good - it keeps the things you should not be concerned about outside your reach.

When the IDbContextOptionsExtension.ApplyServices(IServiceCollection services) method gets called, the services parameter is EF Core's internal IServiceCollection. Back to the example we were working with - if the IUserProvider implementation has its own dependencies that are registered in the applications dependency injection container, but we register the class with EF Core's container, there is no way to resolve those dependencies. So it's impossible to access  IHttpContextAccessor or any other dependencies the class might need.

As usual, there are multiple ways to address this.

Option #1

This option will require the end user to register the IUserProvider separately in the application's DI container (like in the second approach). Then, it's possible to retrieve the service through the DbContext instance that's available in the interceptor's overloaded methods, e.g.:

class UserContextInterceptor : DbConnectionInterceptor
{
    public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
    {
        var userProvider = eventData.Context?.GetService<IUserProvider>();
        var user = userProvider?.GetCurrentUser();
        if (string.IsNullOrEmpty(user))
        {
            return;
        }

        /// Do things...
    }
}

The DbContext.GetService<TService>() method first checks the EF Core's internal IServiceProvider for TService, and if it's not there, it looks into the application's dependency injection container.

This way of solving the issue makes it similar to the second approach and does not need the IDbContextOptionsExtension to work.

Option #2

This option is pretty radical, and I would not recommend it unless you really know what you are doing. There is a way to make the EF Core use the application's IServiceProvider for its internal services. If all services are in the same DI container, there will be no issues with resolving the dependencies.

This would require the library author to wrap the following code (and its equivalents for other database engines) in a pretty extension method and encourage the end user to use it:

services.AddEntityFrameworkSqlServer()
        .AddDbContext<ApplicationDbContext>(
            (provider, options) =>
            {
                options
                    .UseSqlServer("<connection-string>")
                    .UseInternalServiceProvider(provider);
            });

In my opinion, this really is the last resort. It's database-specific and breaks the boundaries the EF Core developers worked really hard to maintain.

Option #3

If you read the Option #1 part carefully, you might have caught the part where DbContext.GetService<TService>() method resolved services from the applications IServiceProvider. This is what the third, last option will utilize. Here, the DbContextOptionsBuilder extension gets updated to look like this:

public static DbContextOptionsBuilder AddInterceptor<TUserProvider>(this DbContextOptionsBuilder optionsBuilder, IServiceProvider provider) where TUserProvider : class, IUserProvider
{
    ((IDbContextOptionsBuilderInfrastructure)optionsBuilder)
        .AddOrUpdateExtension(
            new EfCoreOptionsExtension(
                services =>
                {
                    services.AddScoped<IUserProvider>(
                        provider =>
                        {
                            var applicationServiceProvider = provider
                                .GetService<IDbContextOptions>()?
                                .FindExtension<CoreOptionsExtension>()?
                                .ApplicationServiceProvider;
                            if (applicationServiceProvider == null)
                            {
                                return new EmptyUserProvider();
                            }

                            var userProvider = ActivatorUtilities.GetServiceOrCreateInstance<TUserProvider>(applicationServiceProvider);
                            return userProvider;
                        });
                    services.AddScoped<IInterceptor, UserContextInterceptor>();
                }));

    return optionsBuilder;
}

I know that this may look scary - but it's really not. First, we need to retrieve the application's IServiceProvider, which is hidden inside the CoreOptionsExtension that's, in turn, a part of the IDbContextOptions. If that cannot be retrieved, then either an instance of a dummy IUserProvider implementation can be returned, or an exception can be raised. Here it's the former. Then, using the ActivatorUtilities utility class, the real implementation instance can be created, and its dependencies will be resolved using the application's DI container.

Conclusion

As you can see, even though there are at least three ways to overcome the problem, it's not a trivial issue. EF Core developers are aware that resolving services from the application's dependency injection container is not the greatest experience, so there is hope that it will be improved in future releases. If you want to read more about the problem and how the above solutions came to be, here are a few links that should point you in the right direction:

https://blog.oneunicorn.com/2016/10/27/dependency-injection-in-ef-core-1-1/

Ability to register IInterceptor without an IDbContextOptionsExtension · Issue #21578 · dotnet/efcore
I want to use Azure Managed Identities to connect to an Azure SQL instance. I’m trying to use a DbConnectionInterceptor to handle the ConnectionOpeningAsync “event” to use the Azure.Identity SDK to...
Ability to resolve the IServiceProvider used to lease an instance of a pooled DbContext · Issue #23559 · dotnet/efcore
(Especially) with the newly released support for SaveChanges interceptors, we often want to communicate back with our application services. e.g. We may want to implement an audit handler that logs ...
Better way to get application services from within a derived DbContext instance · Issue #7185 · dotnet/efcore
I am trying to get a service from within a derived DbContext using the following code: ((IInfrastructure<IServiceProvider>)this).GetService<IMyService>(); The IMyService was previously registered i...

In the end, I wanted to apologize if the article caused you any headaches.


There's one post in particular that I wanted to recommend when it comes to interceptors:

Providing Multitenancy with ASP.NET Core and PostgreSQL Row Level Security

It's a great read and an interesting topic, so make sure to give it a go!

Cover photo by Garett Mizunaka on Unsplash

Show Comments