基於 EFCore 的 Interceptor 實作自動更新內容
Intro
我們的表中經常會有一些建立時間、最後更新時間之類的欄位,每次建立或更新的時候都要更新感覺好繁瑣,於是就想借助於 efcore 裏面的 Interceptor 來實作這些內容的自動更新,這樣就無需每次都去指定這些欄位了
Sample
首先我們基於建一個測試用的
DbContext
,程式碼如下:
file sealed class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(options)
{
public DbSet<Job> Jobs { get; set; } = default!;
}
public classJob
{
publicint Id { get; set; }
[StringLength(120)]
public required string Title { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
這裏定義了一個
CreatedAt
和
UpdatdAt
內容,我們就以這兩個欄位來做測試
為了實作自動更新,我們需要建立一個
SaveChangesInterceptor
實作如下:
file sealed classSavingInterceptor : SaveChangesInterceptor
{
publicoverride InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
ArgumentNullException.ThrowIfNull(eventData.Context);
BeforeSaveChanges(eventData.Context);
returnbase.SavingChanges(eventData, result);
}
publicoverride ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = new CancellationToken())
{
ArgumentNullException.ThrowIfNull(eventData.Context);
BeforeSaveChanges(eventData.Context);
returnbase.SavingChangesAsync(eventData, result, cancellationToken);
}
privatestaticvoidBeforeSaveChanges(DbContext dbContext)
{
foreach (var entry in dbContext.ChangeTracker.Entries<Job>())
{
var entity = entry.Entity;
switch (entry.State)
{
case EntityState.Added:
entity.CreatedAt = entity.UpdatedAt = DateTimeOffset.Now;
break;
case EntityState.Added or EntityState.Modified:
entity.UpdatedAt = DateTimeOffset.Now;
break;
}
}
}
}
其他的測試程式碼如下:
conststring sqlServerConnectionString = "Server=.;Database=MyTestDb;User Id=sa;Password=Test1234;TrustServerCertificate=True;";
awaitusingvar services = new ServiceCollection()
.AddLogging(lb => lb.AddDelegateLogger((category, level, exception, msg) =>
{
Console.WriteLine($"[{level}][{category}] {msg}\n{exception}");
}))
.AddDbContext<TestDbContext>(options =>
{
options.AddInterceptors(new SavingInterceptor());
options.UseSqlServer(sqlServerConnectionString);
})
.BuildServiceProvider()
;
awaitusingvar scope = services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TestDbContext>();
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();
var job = new Job() { Title = "test" };
dbContext.Jobs.Add(job);
await dbContext.SaveChangesAsync();
await Task.Delay(1000);
job.Title = "test2";
await dbContext.SaveChangesAsync();
var jobs = await dbContext.Jobs.AsNoTracking().ToArrayAsync();
Console.WriteLine(JsonSerializer.Serialize(jobs));
AddDelegateLogger
這裏是指定一個
這裏先新增了一條數據,然後等待一秒後再修改這條數據以便於方便地區分
CreatedAt
和
UpdatedAt
,最後把數據查詢並打印出來可以看到上述程式碼中在新增和更新數據時並未指定
CreatedAt
和
UpdatedAt
,我們來執行看一下效果,輸出結果如下:
從輸出結果可以看出來,
UpdatedAt
/
CreatedAt
均已被更新,並且
UpdatedAt
要比
CreatedAt
大 1 秒
從更新的 sql 語句也可以看得出來,更新的欄位不僅僅包含了我們指定的
Title
也包含了
UpdatedAt
欄位
Enhancement
上面我們直接指定了對應的 Entity, 如果有多個 Entity 的話怎麽處理呢,多寫幾遍也可以工作但是就比較繁瑣,我們可以透過
Basentity
+ 模式匹配來處理,範例如下:
可以讓所有的 entity 都繼承於一個 base 比如可以實作這個介面
publicinterfaceIEntityWithCreatedUpdatedAt
{
DateTimeOffset CreatedAt { get; set; }
DateTimeOffset UpdatedAt { get; set; }
}
然後再使用
Entities()
方法獲取所有的物件,再根據模式匹配過濾掉不需要處理的物件,其他的和上面的邏輯就是一樣的了,新增 Entity 的時候只需要繼承或實作
IEntityWithCreatedUpdatedAt
即可
foreach (var entry in dbContext.ChangeTracker.Entries())
{
if (entry.Entity is not IEntityWithCreatedUpdatedAt entity) continue;
switch (entry.State)
{
case EntityState.Added:
entity.CreatedAt = entity.UpdatedAt = DateTimeOffset.Now;
break;
case EntityState.Added or EntityState.Modified:
entity.UpdatedAt = DateTimeOffset.Now;
break;
}
}
當 entity 沒有實作
IEntityWithCreatedUpdatedAt
時
當實作了的時候:
References
https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors
https://github.com/WeihanLi/SamplesInPractice/blob/main/EFSamples/EFSamples/AutoUpdateSample.cs