- A+
EFCore 5 新特性 SaveChangesInterceptor
Intro
之前 EF Core 5 还没正式发布的时候有发布过一篇关于 SaveChangesEvents 的文章,有需要看可以移步到 efcore 新特性 SaveChanges Events,在后面的版本中又加入了 Interceptor 的支持,可以更方便的实现 SaveChanges
事件的复用, 今天主要介绍一下通过 SaveChangesInterceptor
来实现日志审计
SaveChangesInterceptor
源码实现:
public interface ISaveChangesInterceptor : IInterceptor { /// <summary> /// Called at the start of <see cref="M:DbContext.SaveChanges" />. /// </summary> /// <param name="eventData"> Contextual information about the <see cref="DbContext" /> being used. </param> /// <param name="result"> /// Represents the current result if one exists. /// This value will have <see cref="InterceptionResult{Int32}.HasResult" /> set to <see langword="true" /> if some previous /// interceptor suppressed execution by calling <see cref="InterceptionResult{Int32}.SuppressWithResult" />. /// This value is typically used as the return value for the implementation of this method. /// </param> /// <returns> /// If <see cref="InterceptionResult{Int32}.HasResult" /> is false, the EF will continue as normal. /// If <see cref="InterceptionResult{Int32}.HasResult" /> is true, then EF will suppress the operation it /// was about to perform and use <see cref="InterceptionResult{Int32}.Result" /> instead. /// A normal implementation of this method for any interceptor that is not attempting to change the result /// is to return the <paramref name="result" /> value passed in. /// </returns> InterceptionResult<int> SavingChanges( [NotNull] DbContextEventData eventData, InterceptionResult<int> result); /// <summary> /// <para> /// Called at the end of <see cref="M:DbContext.SaveChanges" />. /// </para> /// <para> /// This method is still called if an interceptor suppressed creation of a command in <see cref="SavingChanges" />. /// In this case, <paramref name="result" /> is the result returned by <see cref="SavingChanges" />. /// </para> /// </summary> /// <param name="eventData"> Contextual information about the <see cref="DbContext" /> being used. </param> /// <param name="result"> /// The result of the call to <see cref="M:DbContext.SaveChanges" />. /// This value is typically used as the return value for the implementation of this method. /// </param> /// <returns> /// The result that EF will use. /// A normal implementation of this method for any interceptor that is not attempting to change the result /// is to return the <paramref name="result" /> value passed in. /// </returns> int SavedChanges( [NotNull] SaveChangesCompletedEventData eventData, int result); /// <summary> /// Called when an exception has been thrown in <see cref="M:DbContext.SaveChanges" />. /// </summary> /// <param name="eventData"> Contextual information about the failure. </param> void SaveChangesFailed( [NotNull] DbContextErrorEventData eventData); /// <summary> /// Called at the start of <see cref="M:DbContext.SaveChangesAsync" />. /// </summary> /// <param name="eventData"> Contextual information about the <see cref="DbContext" /> being used. </param> /// <param name="result"> /// Represents the current result if one exists. /// This value will have <see cref="InterceptionResult{Int32}.HasResult" /> set to <see langword="true" /> if some previous /// interceptor suppressed execution by calling <see cref="InterceptionResult{Int32}.SuppressWithResult" />. /// This value is typically used as the return value for the implementation of this method. /// </param> /// <param name="cancellationToken"> The cancellation token. </param> /// <returns> /// If <see cref="InterceptionResult{Int32}.HasResult" /> is false, the EF will continue as normal. /// If <see cref="InterceptionResult{Int32}.HasResult" /> is true, then EF will suppress the operation it /// was about to perform and use <see cref="InterceptionResult{Int32}.Result" /> instead. /// A normal implementation of this method for any interceptor that is not attempting to change the result /// is to return the <paramref name="result" /> value passed in. /// </returns> ValueTask<InterceptionResult<int>> SavingChangesAsync( [NotNull] DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default); /// <summary> /// <para> /// Called at the end of <see cref="M:DbContext.SaveChangesAsync" />. /// </para> /// <para> /// This method is still called if an interceptor suppressed creation of a command in <see cref="SavingChangesAsync" />. /// In this case, <paramref name="result" /> is the result returned by <see cref="SavingChangesAsync" />. /// </para> /// </summary> /// <param name="eventData"> Contextual information about the <see cref="DbContext" /> being used. </param> /// <param name="result"> /// The result of the call to <see cref="M:DbContext.SaveChangesAsync" />. /// This value is typically used as the return value for the implementation of this method. /// </param> /// <param name="cancellationToken"> The cancellation token. </param> /// <returns> /// The result that EF will use. /// A normal implementation of this method for any interceptor that is not attempting to change the result /// is to return the <paramref name="result" /> value passed in. /// </returns> ValueTask<int> SavedChangesAsync( [NotNull] SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default); /// <summary> /// Called when an exception has been thrown in <see cref="M:DbContext.SaveChangesAsync" />. /// </summary> /// <param name="eventData"> Contextual information about the failure. </param> /// <param name="cancellationToken"> The cancellation token. </param> /// <returns> A <see cref="Task" /> representing the asynchronous operation. </returns> Task SaveChangesFailedAsync( [NotNull] DbContextErrorEventData eventData, CancellationToken cancellationToken = default); }
为了比较方便的实现自己需要的 Interceptor,微软还提供了一个 SaveChangesInterceptor
抽象类,这样只需要继承于这个类,重写自己需要的方法即可,实现比较简单,就是实现了 ISaveChangesInterceptor
接口,然后接口的实现基本都是空的虚方法,根据需要重写即可
源码链接:https://github.com/dotnet/efcore/blob/v5.0.0/src/EFCore/Diagnostics/SaveChangesInterceptor.cs
使用 SaveChangesInterceptor 实现自动审计
简单写了一个测试的审计拦截器
public class AuditInterceptor : SaveChangesInterceptor { public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { var changesList = new List<CompareModel>(); foreach (var entry in eventData.Context.ChangeTracker.Entries<Post>()) { if (entry.State == EntityState.Added) { changesList.Add(new CompareModel() { OriginalValue = null, NewValue = entry.CurrentValues.ToObject(), }); } else if (entry.State == EntityState.Deleted) { changesList.Add(new CompareModel() { OriginalValue = entry.OriginalValues.ToObject(), NewValue = null, }); } else if (entry.State == EntityState.Modified) { changesList.Add(new CompareModel() { OriginalValue = entry.OriginalValues.ToObject(), NewValue = entry.CurrentValues.ToObject(), }); } Console.WriteLine($"change list:{changesList.ToJson()}"); } return base.SavingChanges(eventData, result); } public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) { Console.WriteLine($"changes:{eventData.EntitiesSavedCount}"); return base.SavedChanges(eventData, result); } private class CompareModel { public object OriginalValue { get; set; } public object NewValue { get; set; } } }
实际应用的话还需要根据自己的场景做一些修改和测试
测试 DbContext 示例,这里使用了一个简单的 InMemory 做了一个测试:
public class TestDbContext : DbContext { public TestDbContext(DbContextOptions<TestDbContext> dbContextOptions) : base(dbContextOptions) { } public DbSet<Post> Posts { get; set; } } public class Post { [Key] public int Id { get; set; } public string Author { get; set; } public string Title { get; set; } public DateTime PostedAt { get; set; } }
测试代码:
var services = new ServiceCollection(); services.AddDbContext<TestDbContext>(options => { options.UseInMemoryDatabase("Tests") //.LogTo(Console.WriteLine) // EF Core 5 中新的更简洁的日志记录方式 .AddInterceptors(new AuditInterceptor()) ; }); using var provider = services.BuildServiceProvider(); using (var scope = provider.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<TestDbContext>(); dbContext.Posts.Add(new Post() { Id = 1, Author = "test", Title = "test", PostedAt = DateTime.UtcNow }); dbContext.SaveChanges(); var post = dbContext.Posts.Find(1); post.Author = "test2"; dbContext.SaveChanges(); dbContext.Posts.Remove(post); dbContext.SaveChanges(); }
输出结果(输出结果的如果数据为 null 就会被忽略掉,所以对于新增的数据实际是没有原始值的,对于删除的数据没有新的值):
More
EF Core 5 还有很多新的特性,有需要的小伙伴可以看一下官方文档的介绍~
上述源码可以在 Github 上获取 https://github.com/WeihanLi/SamplesInPractice/blob/master/EF5Samples/SaveChangesInterceptorTest.cs