
最近在 GitHub 上看到一个 issue 结合 Aspire 使用 EF 时遇到想要从从依赖注入中注册 Interceptor ,之前我们也分享过一次可以使用基于 IServiceProvider 来注册,但是 Aspire 的封装注册方法的时候不支持 IServiceProvider 参数,

导致没有办法从依赖注入中获取 service,于是看了下 EF 的注册实现分享了一个可以 替代的解决方法,如果你也在使用 Aspire 的 EF 扩展也可以试一下这个方式来注册 Interceptor
由于不能直接使用基于 IServiceProvider 的注册方法来注册了,我们自己来研究下基于 IServiceProvider 的注册过程是什么样的

AddDbContext

AddDbContext2

AddCoreServices

ConfigureDbContext

CreateDbContextOptions
我们可以看到最后注册了一个 IDbContextOptionsConfiguration<TContext> 的服务,类似于 options 模式里的 configure service 一样配置 DbContextOptions 所以我们可以单独注册一个 IDbContextOptionsConfiguration<TContext> 服务来配置 DbContextOptions,给 DbContext 注册 Interceptor,所以我们可以封装一个类似下面这样的方法:
public static IServiceCollection AddDbContextInterceptor<TContext, TInterceptor>(
this IServiceCollection services,
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped
)
where TContext : DbContext
where TInterceptor : IInterceptor
{
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction = (sp, builder) =>
{
builder.AddInterceptors(sp.GetRequiredService<TInterceptor>());
};
services.Add(ServiceDescriptor.Describe(typeof(TInterceptor), typeof(TInterceptor), optionsLifetime));
services.Add(ServiceDescriptor.Describe(typeof(IDbContextOptionsConfiguration<TContext>), _ =>
new DbContextOptionsConfiguration<TContext>(optionsAction), optionsLifetime));
return services;
}
通过注册一个 configure service 来从 IServiceProvider 中获取 Interceptor 并配置 DbContext,由于 DbContext 默认的服务声明周期是 Scoped 所以我们也将服务默认注入为 Scoped,为了支持用户自定义 DbContext 服务生命周期我们也支持下自定义服务声明周期
这样定义之后会发现有一个 warning,如下图所示

因为这个 DbContextOptionsConfiguration 是 EF 内部的一个类型,虽然这个类似是 public 但是,不建议直接使用,后续版本可能会发生破坏性的变更,我们可以忽略这个警告,或者自己实现一下这个类型,也比较简单,实现如下:
internal sealed class DbContextOptionsConfiguration<TContext>(
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction
)
: IDbContextOptionsConfiguration<TContext>
whereTContext : DbContext
{
private readonly Action<IServiceProvider, DbContextOptionsBuilder> _optionsAction = optionsAction ?? throw new ArgumentNullException(nameof(optionsAction));
public void Configure(IServiceProvider serviceProvider, DbContextOptionsBuilder optionsBuilder)
{
_optionsAction.Invoke(serviceProvider, optionsBuilder);
}
}
再优化下前面的方法,最后方法定义如下:
public static IServiceCollection AddDbContextInterceptor<TContext, TInterceptor>(
this IServiceCollection services,
ServiceLifetime optionsLifetime = ServiceLifetime.Scoped
)
where TContext : DbContext
where TInterceptor : IInterceptor
{
ArgumentNullException.ThrowIfNull(services);
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction = (sp, builder) =>
{
builder.AddInterceptors(sp.GetRequiredService<TInterceptor>());
};
services.TryAdd(ServiceDescriptor.Describe(typeof(TInterceptor), typeof(TInterceptor), optionsLifetime));
services.Add(ServiceDescriptor.Describe(typeof(IDbContextOptionsConfiguration<TContext>),
_ => new DbContextOptionsConfiguration<TContext>(optionsAction), optionsLifetime));
return services;
}
我们来写一个方法来测试一下,测试代码如下:
var services = new ServiceCollection();
services.AddDbContext<FileTestDbContext>(options =>
{
options.UseInMemoryDatabase("test");
});
services.AddDbContextInterceptor<FileTestDbContext, SavingInterceptor>();
awaitusingvar provider = services.BuildServiceProvider();
usingvar scope = provider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<FileTestDbContext>();
await dbContext.Database.EnsureCreatedAsync();
dbContext.Entities.Add(new TestEntity { Id = 2, Name = "1" });
await dbContext.SaveChangesAsync();
file sealed class FileTestDbContext(DbContextOptions<FileTestDbContext> options) : DbContext(options)
{
public DbSet<TestEntity> Entities { get; set; }
}
file sealed class TestEntity
{
publicint Id { get; set; }
publicstring Name { get; set; }
}
file sealed class SavingInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
Console.WriteLine("SavingChangesAsync");
returnbase.SavingChangesAsync(eventData, result, cancellationToken);
}
public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result,
CancellationToken cancellationToken = default)
{
Console.WriteLine("SavedChangesAsync");
returnbase.SavedChangesAsync(eventData, result, cancellationToken);
}
}
我们来验证下,输出结果如下:
SavingChangesAsync
SavedChangesAsync
可以看到我们的 interceptor 有在正常工作了~~