24

EFCore 5 新特性 SaveChangesInterceptor

 3 years ago
source link: http://www.cnblogs.com/weihanli/p/13968771.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

63Y77r.png!mobile

More

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

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

Reference


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK