EFCore 5 新特性 SaveChangesInterceptor

WeihanLi發表於2020-11-14

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

相關文章