如何在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(); } }