當前位置: 妍妍網 > 碼農

基於依賴註入使用 EF Interceptor

2024-06-02碼農

基於依賴註入使用 EF Interceptor

Intro

在上一篇文章中簡單介紹了下 ,有朋友問如何在 interceptor 中使用使用依賴註入

Sample

DbContext sample:


file sealed class BlogPostContext(DbContextOptions<BlogPostContext> options): DbContext(options)
{
public DbSet<BlogPost> Posts { getset; } = default!;
}
public classBlogPost
{
publicint Id { getset; }
[StringLength(64)]
public required string Title { getset; }
public DateTimeOffset UpdatedAt { getset; }
[StringLength(64)]
publicstring UpdatedBy { getset; } = default!;
}


這裏 BlogPost 定義了一個 UpdatedBy 我們也透過 interceptor 來實作自動更新,與之前不同的是,我們透過從依賴註入中獲取 UpdatedBy 資訊,我們定義一個 IUserIdProvider 來獲取更新使用者的資訊,並且提供了一個預設的 UserIdProvider ,實際使用可能要從當前環境上下文中獲取,比如從 HttpContext 中獲取當前使用者的資訊


file interfaceIUserIdProvider
{
string? GetUserId();
}
file sealed classUserIdProvider : IUserIdProvider
{
publicstringGetUserId() => "Admin";
}
file sealed class DIAutoUpdateInterceptor(IUserIdProvider userIdProvider) : SaveChangesInterceptor
{
publicoverride 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;
}
returnbase.SavingChangesAsync(eventData, result, cancellationToken);
}
}

要使用依賴註入註冊的時候,需要透過帶 IServiceProvider 的 options 配置多載,在 AddInterceptor 的時候從 serviceProvider 中獲取或者建立對應的 Interceptor

.AddDbContext<BlogPostContext>((provider, options) =>
{
options.AddInterceptors(provider.GetRequiredService<DIAutoUpdateInterceptor>());
options.UseSqlite(sqlLiteConnectionString);
})

完整的註冊以及使用範例如下:

conststring sqlLiteConnectionString = "DataSource=Blog";
awaitusingvar 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()
;
{
awaitusingvar 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 也將是另外一個例項

{
awaitusingvar scope = services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BlogPostContext>();
dbContext.Posts.Add(new BlogPost()
{
Title = "test3",
});
await dbContext.SaveChangesAsync();
}

輸出結果:

從打印出來的 hashCode 可以看得出來,這次的 hashCode 和之前並不相同,並不是同一個例項

References

  • https://github.com/WeihanLi/SamplesInPractice/blob/main/EFSamples/EFSamples/InterceptorDISample.cs