基于 EFCore 的 Interceptor 实现属性自动更新

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

这里定义了一个 CreatedAtUpdatdAt 属性,我们就以这两个字段来做测试

为了实现自动更新,我们需要创建一个 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 这里是指定一个

这里先新增了一条数据,然后等待一秒后再修改这条数据以便于方便地区分 CreatedAtUpdatedAt,最后把数据查询并打印出来可以看到上述代码中在新增和更新数据时并未指定 CreatedAtUpdatedAt,我们来运行看一下效果,输出结果如下:

191584dead40dda792dafccb3440d3cc.png

output

从输出结果可以看出来,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

1512f4a122c2e878c29bb643e2ea097b.png

IEntityWithCreatedUpdatedAt-not-implemented

当实现了的时候:

9eae57c17308b8e23d84a5eaee416c22.png

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值