• 如何在interceptor中使用使用依赖注入
  • 发布于 1个月前
  • 74 热度
    0 评论
背景
近日,有朋友问如何在 interceptor 中使用使用依赖注入,今天就聊聊这个话题。

先看例子
DbContext sample:
file sealed class BlogPostContext(DbContextOptions<BlogPostContext> options): DbContext(options)
{
    public DbSet<BlogPost> Posts { get; set; } = default!;
}

public class BlogPost
{
    public int Id { get; set; }
    [StringLength(64)]
    public required string Title { get; set; }
    public DateTimeOffset UpdatedAt { get; set; }
    [StringLength(64)]
    public string UpdatedBy { get; set; } = default!;
}
这里 BlogPost 定义了一个 UpdatedBy 我们也通过 interceptor 来实现自动更新,与之前不同的是,我们通过从依赖注入中获取 UpdatedBy 信息,我们定义一个 IUserIdProvider 来获取更新用户的信息,并且提供了一个默认的 UserIdProvider,实际使用可能要从当前环境上下文中获取,比如从 HttpContext 中获取当前用户的信息
file interface IUserIdProvider
{
    // 堆代码 duidaima.com
    string? GetUserId();
}

file sealed class UserIdProvider : IUserIdProvider
{
    public string GetUserId() => "Admin";
}

file sealed class DIAutoUpdateInterceptor(IUserIdProvider userIdProvider) : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Current interceptor hashCode: {GetHashCode()}");
        ArgumentNullException.ThrowIfNull(eventData.Context);
        string? userId = null;
        foreach (var entry in eventData.Context.ChangeTracker.Entries<BlogPost>())
        {
            if (entry.State is not EntityState.Added) continue;
            userId ??= userIdProvider.GetUserId() ?? "";
            
            entry.Entity.UpdatedAt = DateTimeOffset.Now;
            entry.Entity.UpdatedBy = userId;
        }
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}
要使用依赖注入注册的时候,需要通过带 IServiceProvider 的 options 配置重载,在 AddInterceptor 的时候从 serviceProvider 中获取或者创建对应的 Interceptor
.AddDbContext<BlogPostContext>((provider, options) =>
{
    options.AddInterceptors(provider.GetRequiredService<DIAutoUpdateInterceptor>());
    options.UseSqlite(sqlLiteConnectionString);
})
完整的注册以及使用示例如下:
const string sqlLiteConnectionString = "DataSource=Blog";

await using var services = new ServiceCollection()
        .AddLogging(lb => lb.AddDelegateLogger((category, level, exception, msg) =>
        {
            Console.WriteLine($"[{level}][{category}] {msg}\n{exception}");
        }))
        .AddSingleton<IUserIdProvider, UserIdProvider>()
        .AddScoped<DIAutoUpdateInterceptor>()
        .AddDbContext<BlogPostContext>((provider, options) =>
        {
            options.AddInterceptors(provider.GetRequiredService<DIAutoUpdateInterceptor>());
            options.UseSqlite(sqlLiteConnectionString);
        })
        .BuildServiceProvider()
    ;

{
    await using var scope = services.CreateAsyncScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<BlogPostContext>();
    await dbContext.Database.EnsureDeletedAsync();
    await dbContext.Database.EnsureCreatedAsync();

    dbContext.Posts.Add(new BlogPost()
    {
        Title = "test",
    });
    await dbContext.SaveChangesAsync();


    dbContext.Posts.Add(new BlogPost()
    {
        Title = "test2",
    });
    await dbContext.SaveChangesAsync();

    var posts = await dbContext.Posts.AsNoTracking().ToArrayAsync();
    Console.WriteLine(JsonSerializer.Serialize(posts));
}
从 ServiceProvider 中创建 Interceptor 实例时需要注册,可以注册为 Singleton 也可以注册为 Scoped,注册时创建实例的 IServiceProvider 和 DbContext 的 lifetime 是一样的,而默认的 DbContext 是 Scoped 也就意味着 interceptor 也可以是 Scoped

输出结果如下:

这里使用的是一个 DbContext 在同一个 scope 里,所以其实 Interceptor 也是同一个实例
如果我们在另外一个 scope 另外一个 DbContext 中,我们的 Interceptor 也将是另外一个实例
{
    await using var scope = services.CreateAsyncScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<BlogPostContext>();
    dbContext.Posts.Add(new BlogPost()
    {
        Title = "test3",
    });
    await dbContext.SaveChangesAsync();
}
输出结果:

从打印出来的 hashCode 可以看得出来,这次的 hashCode 和之前并不相同,并不是同一个实例
参考
https://github.com/WeihanLi/SamplesInPractice/blob/main/EFSamples/EFSamples/InterceptorDISample.cs
用户评论