zoukankan      html  css  js  c++  java
  • EFCore 5 新特性 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 就会被忽略掉,所以对于新增的数据实际是没有原始值的,对于删除的数据没有新的值):

    More

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

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

    Reference

  • 相关阅读:
    Torchkeras,一个源码不足300行的深度学习框架
    【知乎】语义分割该如何走下去?
    【SDOI2017】天才黑客(前后缀优化建图 & 最短路)
    【WC2014】紫荆花之恋(替罪羊重构点分树 & 平衡树)
    【SDOI2017】相关分析(线段树)
    【学习笔记】分治法最短路小结
    【CH 弱省互测 Round #1 】OVOO(可持久化可并堆)
    【学习笔记】K 短路问题详解
    【学习笔记】浅析平衡树套线段树 & 带插入区间K小值
    【APIO2020】交换城市(Kruskal重构树)
  • 原文地址:https://www.cnblogs.com/weihanli/p/13968771.html
Copyright © 2011-2022 走看看