如何在SaveContext上更新已修改和已删除的实体?

目标是跟踪谁更改并删除了一个实体。

所以我有一个实现接口的实体:

interface IAuditable { string ModifiedBy {get;set;} } class User: IAuditable { public int UserId {get;set;} public string UserName {get;set;} public string ModifiedBy {get;set;} 1541692055 public byte[] RowVersion { get; set; } } 

现在,实体删除操作的代码可能如下所示:

 User user = context.Users.First(); user.ModifiedBy = CurrentUser.Name; context.Users.Remove(employer); context.SaveContext(); 

实际上: ModifiedBy 更新将永远不会执行 (当我的db历史触发器期望“处理”它时)。 只有删除语句才会在DB 上执行

我想知道如果实体被修改,如何强制EF Core“更新”已删除的实体/条目(实现特定的接口)。

注意: RowVersion增加了额外的复杂性。

PS手动添加额外的SaveContext调用 – 当然是一个选项,但我希望有一个通用的解决方案:许多各种更新和删除,然后一个SaveContext进行所有分析。

要在SaveContext收集var deletedEntries = entries.Where(e => e.State == EntityState.Deleted && isAuditable(e))之前手动更新这些属性,它不是一个选项,因为它可能破坏EF Core锁定订单管理并因此引发死锁。

最明确的解决方案是保留一个SaveContext调用,但在EF CORE调用DELETE之前在可审计字段上注入UPDATE语句。 怎么做到这一点? 可能有人已经有了解决方案吗?

替代方案可以是“在删除时不要撰写DELETE语句,而是调用可以接受可审计字段作为参数的存储过程”

我想知道如何在EF调用其“DELETE语句”之前注入我的“UPDATE语句”? 我们有这样的API吗?

有趣的问题。 在撰写本文时(EF Core 2.1.3),没有这样的公共 API。 以下解决方案基于内部API,幸运的是,在EF Core中,可以在典型的内部API免责声明下公开访问:

此API支持Entity Framework Core基础结构,不能直接在您的代码中使用。 此API可能会在将来的版本中更改或删除。

现在的解决方案。 负责修改命令创建的服务称为ICommandBatchPreparer

一种服务,用于为给定的IUpdateEntry列表所代表的实体准备ModificationCommandBatch列表。

它包含一个名为BatchCommands方法:

创建插入/更新/删除由给定IUpdateEntry列表表示的实体所需的命令批处理。

具有以下签名:

 public IEnumerable BatchCommands( IReadOnlyList entries); 

CommandBatchPreparer类中的默认实现。

我们将使用自定义实现替换该服务,该实现将使用“已修改”条目扩展列表并使用基本实现来执行实际工作。 由于批处理基本上是按依赖性排序的修改命令列表,然后是按类型 Delete ,在Update之前,我们将首先使用单独的批处理(更新命令),然后连接其余部分。

生成的修改命令基于IUpdateEntry

传递给数据库提供程序的信息,用于将实体的更改保存到数据库。

幸运的是它是一个接口,因此我们将为其他“修改”条目以及相应的删除条目提供我们自己的实现(稍后将详细介绍)。

首先,我们将创建一个基本实现,它简单地将调用委托给底层对象,从而允许我们稍后只覆盖对我们要实现的目标至关重要的方法:

 class DelegatingEntry : IUpdateEntry { public DelegatingEntry(IUpdateEntry source) { Source = source; } public IUpdateEntry Source { get; } public virtual IEntityType EntityType => Source.EntityType; public virtual EntityState EntityState => Source.EntityState; public virtual IUpdateEntry SharedIdentityEntry => Source.SharedIdentityEntry; public virtual object GetCurrentValue(IPropertyBase propertyBase) => Source.GetCurrentValue(propertyBase); public virtual TProperty GetCurrentValue(IPropertyBase propertyBase) => Source.GetCurrentValue(propertyBase); public virtual object GetOriginalValue(IPropertyBase propertyBase) => Source.GetOriginalValue(propertyBase); public virtual TProperty GetOriginalValue(IProperty property) => Source.GetOriginalValue(property); public virtual bool HasTemporaryValue(IProperty property) => Source.HasTemporaryValue(property); public virtual bool IsModified(IProperty property) => Source.IsModified(property); public virtual bool IsStoreGenerated(IProperty property) => Source.IsStoreGenerated(property); public virtual void SetCurrentValue(IPropertyBase propertyBase, object value) => Source.SetCurrentValue(propertyBase, value); public virtual EntityEntry ToEntityEntry() => Source.ToEntityEntry(); } 

现在是第一个自定义条目:

 class AuditUpdateEntry : DelegatingEntry { public AuditUpdateEntry(IUpdateEntry source) : base(source) { } public override EntityState EntityState => EntityState.Modified; public override bool IsModified(IProperty property) { if (property.Name == nameof(IAuditable.ModifiedBy)) return true; return false; } public override bool IsStoreGenerated(IProperty property) => property.ValueGenerated.ForUpdate() && (property.AfterSaveBehavior == PropertySaveBehavior.Ignore || !IsModified(property)); } 

首先,我们将源状态从Deleted “修改”为Modified 。 然后我们修改IsModified方法,该方法对Deleted条目返回false ,以便为可审计属性返回true ,从而强制它们包含在update命令中。 最后,我们修改了IsStoreGenerated方法,该方法对Deleted条目也返回false ,以返回Modified条目( EF Core代码 )的相应结果。 这需要让EF Core在RowVersion等更新时正确处理数据库生成的值。 执行该命令后,EF Core将使用从数据库返回的值调用SetCurrentValue 。 正常的Deleted条目和正常的Deleted Modified条目不会传播到其实体。

这导致我们需要第二个自定义条目,它将包装原始条目并且还将用作AuditUpdateEntry源,因此将从中接收SetCurrentValue 。 它将在内部存储接收的值,从而保持原始实体状态不变,并将它们视为“当前”和“原始”。 这是必不可少的,因为delete命令将在更新后执行,如果RowVersion没有将新值返回为“original”,则生成的delete命令将失败。

这是实施:

 class AuditDeleteEntry : DelegatingEntry { public AuditDeleteEntry(IUpdateEntry source) : base(source) { } Dictionary updatedValues; public override object GetCurrentValue(IPropertyBase propertyBase) { if (updatedValues != null && updatedValues.TryGetValue(propertyBase, out var value)) return value; return base.GetCurrentValue(propertyBase); } public override object GetOriginalValue(IPropertyBase propertyBase) { if (updatedValues != null && updatedValues.TryGetValue(propertyBase, out var value)) return value; return base.GetOriginalValue(propertyBase); } public override void SetCurrentValue(IPropertyBase propertyBase, object value) { if (updatedValues == null) updatedValues = new Dictionary(); updatedValues[propertyBase] = value; } } 

通过这两个自定义条目,我们可以实现自定义命令批处理构建器:

 class AuditableCommandBatchPreparer : CommandBatchPreparer { public AuditableCommandBatchPreparer(CommandBatchPreparerDependencies dependencies) : base(dependencies) { } public override IEnumerable BatchCommands(IReadOnlyList entries) { List auditEntries = null; List auditUpdateEntries = null; for (int i = 0; i < entries.Count; i++) { var entry = entries[i]; if (entry.EntityState == EntityState.Deleted && typeof(IAuditable).IsAssignableFrom(entry.EntityType.ClrType)) { if (auditEntries == null) { auditEntries = entries.Take(i).ToList(); auditUpdateEntries = new List(); } var deleteEntry = new AuditDeleteEntry(entry); var updateEntry = new AuditUpdateEntry(deleteEntry); auditEntries.Add(deleteEntry); auditUpdateEntries.Add(updateEntry); } else { auditEntries?.Add(entry); } } return auditEntries != null ? base.BatchCommands(auditUpdateEntries).Concat(base.BatchCommands(auditEntries)) : base.BatchCommands(entries); } } 

我们差不多完成了。 添加帮助方法来注册我们的服务:

 public static class AuditableExtensions { public static DbContextOptionsBuilder AddAudit(this DbContextOptionsBuilder optionsBuilder) { optionsBuilder.ReplaceService(); return optionsBuilder; } } 

从你DbContext调用它DbContext派生类OnConfiguring覆盖:

 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // ... optionsBuilder.AddAudit(); } 

你完成了

所有这些都是为了获得想法而手动填充的单个可审计字段。 它可以使用更多可审计字段进行扩展,注册自定义可审计字段提供程序服务并自动填充插入/更新/删除操作等的值。


PS完整代码

 using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Update; using Microsoft.EntityFrameworkCore.Update.Internal; using Auditable.Internal; namespace Auditable { public interface IAuditable { string ModifiedBy { get; set; } } public static class AuditableExtensions { public static DbContextOptionsBuilder AddAudit(this DbContextOptionsBuilder optionsBuilder) { optionsBuilder.ReplaceService(); return optionsBuilder; } } } namespace Auditable.Internal { class AuditableCommandBatchPreparer : CommandBatchPreparer { public AuditableCommandBatchPreparer(CommandBatchPreparerDependencies dependencies) : base(dependencies) { } public override IEnumerable BatchCommands(IReadOnlyList entries) { List auditEntries = null; List auditUpdateEntries = null; for (int i = 0; i < entries.Count; i++) { var entry = entries[i]; if (entry.EntityState == EntityState.Deleted && typeof(IAuditable).IsAssignableFrom(entry.EntityType.ClrType)) { if (auditEntries == null) { auditEntries = entries.Take(i).ToList(); auditUpdateEntries = new List(); } var deleteEntry = new AuditDeleteEntry(entry); var updateEntry = new AuditUpdateEntry(deleteEntry); auditEntries.Add(deleteEntry); auditUpdateEntries.Add(updateEntry); } else { auditEntries?.Add(entry); } } return auditEntries != null ? base.BatchCommands(auditUpdateEntries).Concat(base.BatchCommands(auditEntries)) : base.BatchCommands(entries); } } class AuditUpdateEntry : DelegatingEntry { public AuditUpdateEntry(IUpdateEntry source) : base(source) { } public override EntityState EntityState => EntityState.Modified; public override bool IsModified(IProperty property) { if (property.Name == nameof(IAuditable.ModifiedBy)) return true; return false; } public override bool IsStoreGenerated(IProperty property) => property.ValueGenerated.ForUpdate() && (property.AfterSaveBehavior == PropertySaveBehavior.Ignore || !IsModified(property)); } class AuditDeleteEntry : DelegatingEntry { public AuditDeleteEntry(IUpdateEntry source) : base(source) { } Dictionary updatedValues; public override object GetCurrentValue(IPropertyBase propertyBase) { if (updatedValues != null && updatedValues.TryGetValue(propertyBase, out var value)) return value; return base.GetCurrentValue(propertyBase); } public override object GetOriginalValue(IPropertyBase propertyBase) { if (updatedValues != null && updatedValues.TryGetValue(propertyBase, out var value)) return value; return base.GetOriginalValue(propertyBase); } public override void SetCurrentValue(IPropertyBase propertyBase, object value) { if (updatedValues == null) updatedValues = new Dictionary(); updatedValues[propertyBase] = value; } } class DelegatingEntry : IUpdateEntry { public DelegatingEntry(IUpdateEntry source) { Source = source; } public IUpdateEntry Source { get; } public virtual IEntityType EntityType => Source.EntityType; public virtual EntityState EntityState => Source.EntityState; public virtual IUpdateEntry SharedIdentityEntry => Source.SharedIdentityEntry; public virtual object GetCurrentValue(IPropertyBase propertyBase) => Source.GetCurrentValue(propertyBase); public virtual TProperty GetCurrentValue(IPropertyBase propertyBase) => Source.GetCurrentValue(propertyBase); public virtual object GetOriginalValue(IPropertyBase propertyBase) => Source.GetOriginalValue(propertyBase); public virtual TProperty GetOriginalValue(IProperty property) => Source.GetOriginalValue(property); public virtual bool HasTemporaryValue(IProperty property) => Source.HasTemporaryValue(property); public virtual bool IsModified(IProperty property) => Source.IsModified(property); public virtual bool IsStoreGenerated(IProperty property) => Source.IsStoreGenerated(property); public virtual void SetCurrentValue(IPropertyBase propertyBase, object value) => Source.SetCurrentValue(propertyBase, value); public virtual EntityEntry ToEntityEntry() => Source.ToEntityEntry(); } }