EFCore 5 新特性 SaveChangesInterceptor

今天主要介绍一下通过 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 就会被忽略掉,所以对于新增的数据实际是没有原始值的,对于删除的数据没有新的值):
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值