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
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
{
}
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
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:
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