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

How to UPDATE modified and deleted entities on 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;}
   [Timestamp]
   public byte[] RowVersion { get; set; }
}

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

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

实际上:ModifiedBy 更新将永远不会执行(当我的数据库历史记录触发 expect 时 "handle" 它)。 只会在数据库上执行删除语句

我想知道如果实体被修改,如何强制删除 EF Core "update" entities/entries(实现特定接口)。

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

P.S。 手动调用额外的 SaveContext - 当然是一个选项,但我想要一个通用的解决方案:许多不同的更新和删除,然后一个 SaveContext 进行所有分析。

在收集 SaveContext 之前手动更新这些属性 var deletedEntries = entries.Where(e => e.State == EntityState.Deleted && isAuditable(e)) 这不是一个选项,因为它会破坏 EF Core 锁顺序管理,从而引发死锁。

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

备选方案可以是 "on delete do not compose DELETE statement but call stored procedure that can accept auditable fields as paramaters"

I want to know how to inject my "UPDATE statement" just before EF call its "DELETE statement"? Do we have such API?

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

This API supports the Entity Framework Core infrastructure and is not intended to be used directly from your code. This API may change or be removed in future releases.

现在是解决方案。负责创建修改命令的服务叫做ICommandBatchPreparer:

A service for preparing a list of ModificationCommandBatchs for the entities represented by the given list of IUpdateEntrys.

它包含一个名为 BatchCommands 的方法:

Creates the command batches needed to insert/update/delete the entities represented by the given list of IUpdateEntrys.

具有以下签名:

public IEnumerable<ModificationCommandBatch> BatchCommands(
    IReadOnlyList<IUpdateEntry> entries);

CommandBatchPreparer class.

中的默认实现

我们将用自定义实现替换该服务,自定义实现将使用“修改过的”条目扩展列表,并使用基本实现来完成实际工作。由于批处理基本上是按依赖项排序的修改命令列表,然后 按类型 DeleteUpdate 之前,我们将使用单独的批处理先更新命令,然后再连接其余命令。

生成的修改命令基于IUpdateEntry:

The information passed to a database provider to save changes to an entity to the database.

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

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

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<TProperty>(IPropertyBase propertyBase) => Source.GetCurrentValue<TProperty>(propertyBase);
    public virtual object GetOriginalValue(IPropertyBase propertyBase) => Source.GetOriginalValue(propertyBase);
    public virtual TProperty GetOriginalValue<TProperty>(IProperty property) => Source.GetOriginalValue<TProperty>(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 方法,其中 returns false for Deleted entrys to return true for the auditable properties,从而强制他们是包含在更新命令中。最后,我们修改 IsStoreGenerated 方法,该方法还 returns false for Deleted entries 到 return Modified 条目的相应结果(EF Core code).这是让 EF Core 正确处理更新时数据库生成的值所必需的,例如 RowVersion。执行该命令后,EF Core 将使用从数据库中获取的值 return 调用 SetCurrentValue。对于正常的 Deleted 条目和正常的 Modified 条目不会发生这种情况会传播到它们的实体。

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

实现如下:

class AuditDeleteEntry : DelegatingEntry
{
    public AuditDeleteEntry(IUpdateEntry source) : base(source) { }
    Dictionary<IPropertyBase, object> 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<IPropertyBase, object>();
        updatedValues[propertyBase] = value;
    }
}

有了这两个自定义条目,我们就可以实施自定义命令批处理生成器了:

class AuditableCommandBatchPreparer : CommandBatchPreparer
{
    public AuditableCommandBatchPreparer(CommandBatchPreparerDependencies dependencies) : base(dependencies) { }

    public override IEnumerable<ModificationCommandBatch> BatchCommands(IReadOnlyList<IUpdateEntry> entries)
    {
        List<IUpdateEntry> auditEntries = null;
        List<AuditUpdateEntry> 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<AuditUpdateEntry>();
                }
                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<ICommandBatchPreparer, AuditableCommandBatchPreparer>();
        return optionsBuilder;
    }
}

并从您那里调用它 DbContext 派生 class OnConfiguring 覆盖:

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

大功告成。

所有这些都是针对手动填充的单个可审计字段,只是为了了解这个想法。它可以扩展更多可审计字段,注册自定义可审计字段提供者服务并自动填充 insert/update/delete 操作等的值


P.S.完整代码

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<ICommandBatchPreparer, AuditableCommandBatchPreparer>();
            return optionsBuilder;
        }
    }
}

namespace Auditable.Internal
{
    class AuditableCommandBatchPreparer : CommandBatchPreparer
    {
        public AuditableCommandBatchPreparer(CommandBatchPreparerDependencies dependencies) : base(dependencies) { }

        public override IEnumerable<ModificationCommandBatch> BatchCommands(IReadOnlyList<IUpdateEntry> entries)
        {
            List<IUpdateEntry> auditEntries = null;
            List<AuditUpdateEntry> 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<AuditUpdateEntry>();
                    }
                    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<IPropertyBase, object> 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<IPropertyBase, object>();
            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<TProperty>(IPropertyBase propertyBase) => Source.GetCurrentValue<TProperty>(propertyBase);
        public virtual object GetOriginalValue(IPropertyBase propertyBase) => Source.GetOriginalValue(propertyBase);
        public virtual TProperty GetOriginalValue<TProperty>(IProperty property) => Source.GetOriginalValue<TProperty>(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();
    }
}

更新: 针对 EF Core 5 更新的完整代码(未测试):

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<ICommandBatchPreparer, AuditableCommandBatchPreparer>();
            return optionsBuilder;
        }
    }
}

namespace Auditable.Internal
{
    class AuditableCommandBatchPreparer : CommandBatchPreparer
    {
        public AuditableCommandBatchPreparer(CommandBatchPreparerDependencies dependencies) : base(dependencies) { }

        public override IEnumerable<ModificationCommandBatch> BatchCommands(IList<IUpdateEntry> entries, IUpdateAdapter updateAdapter)
        {
            List<IUpdateEntry> auditEntries = null;
            List<IUpdateEntry> 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<IUpdateEntry>();
                    }
                    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, updateAdapter).Concat(base.BatchCommands(auditEntries, updateAdapter)) :
                base.BatchCommands(entries, updateAdapter);
        }
    }

    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.GetAfterSaveBehavior() == PropertySaveBehavior.Ignore
                    || !IsModified(property));
    }

    class AuditDeleteEntry : DelegatingEntry
    {
        public AuditDeleteEntry(IUpdateEntry source) : base(source) { }
        Dictionary<IPropertyBase, object> 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 SetStoreGeneratedValue(IProperty property, object value)
        {
            if (updatedValues == null) updatedValues = new Dictionary<IPropertyBase, object>();
            updatedValues[property] = value;
        }
    }

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