EFCore 5 新特性 SaveChangesInterceptor
EFCore 5 新特性 SaveChangesInterceptor
Intro
之前 EF Core 5 还没正式发布的时候有发布过一篇关于 SaveChangesEvents 的文章,有需要看可以移步到 efcore 新特性 SaveChanges Events,在后面的版本中又加入了 Interceptor 的支持,可以更方便的实现 SaveChanges
事件的复用, 今天主要介绍一下通过 SaveChangesInterceptor
来实现日志审计
SaveChangesInterceptor
源码实现:
public interface ISaveChangesInterceptor : IInterceptor
{
///
/// Called at the start of .
///
/// Contextual information about the being used.
///
/// Represents the current result if one exists.
/// This value will have set to if some previous
/// interceptor suppressed execution by calling .
/// This value is typically used as the return value for the implementation of this method.
///
///
/// If is false, the EF will continue as normal.
/// If is true, then EF will suppress the operation it
/// was about to perform and use instead.
/// A normal implementation of this method for any interceptor that is not attempting to change the result
/// is to return the value passed in.
///
InterceptionResult<int> SavingChanges(
[NotNull] DbContextEventData eventData,
InterceptionResult<int> result);
///
///
/// Called at the end of .
///
///
/// This method is still called if an interceptor suppressed creation of a command in .
/// In this case, is the result returned by .
///
///
/// Contextual information about the being used.
///
/// The result of the call to .
/// This value is typically used as the return value for the implementation of this method.
///
///
/// 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 value passed in.
///
int SavedChanges(
[NotNull] SaveChangesCompletedEventData eventData,
int result);
///
/// Called when an exception has been thrown in .
///
/// Contextual information about the failure.
void SaveChangesFailed(
[NotNull] DbContextErrorEventData eventData);
///
/// Called at the start of .
///
/// Contextual information about the being used.
///
/// Represents the current result if one exists.
/// This value will have set to if some previous
/// interceptor suppressed execution by calling .
/// This value is typically used as the return value for the implementation of this method.
///
/// The cancellation token.
///
/// If is false, the EF will continue as normal.
/// If is true, then EF will suppress the operation it
/// was about to perform and use instead.
/// A normal implementation of this method for any interceptor that is not attempting to change the result
/// is to return the value passed in.
///
ValueTaskint>> SavingChangesAsync(
[NotNull] DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default);
///
///
/// Called at the end of .
///
///
/// This method is still called if an interceptor suppressed creation of a command in .
/// In this case, is the result returned by .
///
///
/// Contextual information about the being used.
///
/// The result of the call to .
/// This value is typically used as the return value for the implementation of this method.
///
/// The cancellation token.
///
/// 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 value passed in.
///
ValueTask<int> SavedChangesAsync(
[NotNull] SaveChangesCompletedEventData eventData,
int result,
CancellationToken cancellationToken = default);
///
/// Called when an exception has been thrown in .
///
/// Contextual information about the failure.
/// The cancellation token.
/// A representing the asynchronous operation.
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();
foreach (var entry in
eventData.Context.ChangeTracker.Entries())
{
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 dbContextOptions ) : base(dbContextOptions)
{
}
public DbSet 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(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();
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
https://github.com/dotnet/efcore https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#savechanges-interception-and-events https://www.cnblogs.com/weihanli/p/13416219.html https://github.com/WeihanLi/SamplesInPractice/blob/master/EF5Samples/SaveChangesInterceptorTest.cs