当前位置: 欣欣网 > 码农

基于 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