EFCore 5 新特性 SaveChangesInterceptor

  • A+
所属分类:.NET技术
摘要

之前 EF Core 5 还没正式发布的时候有发布过一篇关于 SaveChangesEvents 的文章,有需要看可以移步到 efcore 新特性 SaveChanges Events,在后面的版本中又加入了 Interceptor 的支持,可以更方便的实现 SaveChanges 事件的复用, 今天主要介绍一下通过 SaveChangesInterceptor 来实现日志审计


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 就会被忽略掉,所以对于新增的数据实际是没有原始值的,对于删除的数据没有新的值):

EFCore 5 新特性 SaveChangesInterceptor

More

EF Core 5 还有很多新的特性,有需要的小伙伴可以看一下官方文档的介绍~

上述源码可以在 Github 上获取 https://github.com/WeihanLi/SamplesInPractice/blob/master/EF5Samples/SaveChangesInterceptorTest.cs

Reference