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:
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:
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:
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/
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:
It's a great read and an interesting topic, so make sure to give it a go!
Cover photo by Garett Mizunaka on Unsplash