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

Rom*_*kij 5 c# entity-framework-core entity-framework-core-2.1

目标是跟踪谁更改和删除了实体。

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

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; }
}
Run Code Online (Sandbox Code Playgroud)

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

User user = context.Users.First();
user.ModifiedBy = CurrentUser.Name;
context.Users.Remove(employer);
context.SaveContext();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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

Iva*_*oev 8

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

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

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

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

用于为给定IUpdateEntry列表表示的实体准备ModificationCommandBatch列表的服务

它包含一个名为 的方法BatchCommands

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

带有以下签名:

public IEnumerable<ModificationCommandBatch> BatchCommands(
    IReadOnlyList<IUpdateEntry> entries);
Run Code Online (Sandbox Code Playgroud)

CommandBatchPreparer类中的默认实现。

我们将使用自定义实现替换该服务,这将使用“修改过的”条目扩展列表并使用基本实现来完成实际工作。由于批处理基本上是按依赖项排序的修改命令列表,然后按类型排序,然后使用Deletebefore 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<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();
}
Run Code Online (Sandbox Code Playgroud)

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

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));
}
Run Code Online (Sandbox Code Playgroud)

首先,我们“修改”源状态从DeletedModified。然后,我们修改IsModified该方法返回falseDeleted条目返回true的审计特性,从而迫使它们被包含在更新命令。最后,我们修改IsStoreGeneratedfalseDeleted条目返回的方法,以返回条目的相应结果ModifiedEF Core 代码)。这是让 EF Core 正确处理更新时数据库生成的值所必需的,例如RowVersion. 执行该命令后,EF Core 将SetCurrentValue使用从数据库返回的值进行调用。正常Deleted条目不会发生这种情况,并且正常Modified条目会传播到它们的实体。

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

这是实现:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们快完成了。添加用于注册我们的服务的辅助方法:

public static class AuditableExtensions
{
    public static DbContextOptionsBuilder AddAudit(this DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.ReplaceService<ICommandBatchPreparer, AuditableCommandBatchPreparer>();
        return optionsBuilder;
    }
}
Run Code Online (Sandbox Code Playgroud)

并从您的DbContext派生类OnConfiguring覆盖调用它:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    // ...
    optionsBuilder.AddAudit();
}
Run Code Online (Sandbox Code Playgroud)

你就完成了。

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


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<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();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:为 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 v