基于 EFCore 的 Interceptor 实现自动更新属性
Intro
我们的表中经常会有一些创建时间、最后更新时间之类的字段,每次创建或更新的时候都要更新感觉好繁琐,于是就想借助于 efcore 里面的 Interceptor 来实现这些属性的自动更新,这样就无需每次都去指定这些字段了
Sample
首先我们基于建一个测试用的 DbContext
,代码如下:
file sealed class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(options)
{
public DbSet<Job> Jobs { get; set; } = default!;
}
public class Job
{
public int 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 class SavingInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
ArgumentNullException.ThrowIfNull(eventData.Context);
BeforeSaveChanges(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = new CancellationToken())
{
ArgumentNullException.ThrowIfNull(eventData.Context);
BeforeSaveChanges(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private static void BeforeSaveChanges(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;
}
}
}
}
其他的测试代码如下:
const string sqlServerConnectionString = "Server=.;Database=MyTestDb;User Id=sa;Password=Test1234;TrustServerCertificate=True;";
await using var 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()
;
await using var 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 比如可以实现这个接口
public interface IEntityWithCreatedUpdatedAt
{
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