當前位置: 妍妍網 > 碼農

基於 EFCore 的 Interceptor 實作內容自動更新

2024-05-31碼農

基於 EFCore 的 Interceptor 實作自動更新內容

Intro

我們的表中經常會有一些建立時間、最後更新時間之類的欄位,每次建立或更新的時候都要更新感覺好繁瑣,於是就想借助於 efcore 裏面的 Interceptor 來實作這些內容的自動更新,這樣就無需每次都去指定這些欄位了

Sample

首先我們基於建一個測試用的 DbContext ,程式碼如下:


file sealed class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(options)
{
public DbSet<Job> Jobs { getset; } = default!;
}
public classJob
{
publicint Id { getset; }
[StringLength(120)]
public required string Title { getset; }
public DateTimeOffset CreatedAt { getset; }
public DateTimeOffset UpdatedAt { getset; }
}

這裏定義了一個 CreatedAt UpdatdAt 內容,我們就以這兩個欄位來做測試

為了實作自動更新,我們需要建立一個 SaveChangesInterceptor 實作如下:


file sealed classSavingInterceptor : SaveChangesInterceptor
{
publicoverride InterceptionResult<intSavingChanges(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 ,我們來執行看一下效果,輸出結果如下:

output

從輸出結果可以看出來, UpdatedAt / CreatedAt 均已被更新,並且 UpdatedAt 要比 CreatedAt 大 1 秒

從更新的 sql 語句也可以看得出來,更新的欄位不僅僅包含了我們指定的 Title 也包含了 UpdatedAt 欄位

Enhancement

上面我們直接指定了對應的 Entity, 如果有多個 Entity 的話怎麽處理呢,多寫幾遍也可以工作但是就比較繁瑣,我們可以透過 Basentity + 模式匹配來處理,範例如下:

可以讓所有的 entity 都繼承於一個 base 比如可以實作這個介面

publicinterfaceIEntityWithCreatedUpdatedAt
{
DateTimeOffset CreatedAt { getset; }
DateTimeOffset UpdatedAt { getset; }
}

然後再使用 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

IEntityWithCreatedUpdatedAt-not-implemented

當實作了的時候:

IEntityWithCreatedUpdatedAt-implemented

References

  • https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors

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