如何在SaveContext上更新已修改和已删除的实体? [英] How to UPDATE modified and deleted entities on SaveContext?

查看:117
本文介绍了如何在SaveContext上更新已修改和已删除的实体?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Goal is to track who had changed and deleted an entity.

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

So I have an entity that implements an interface:

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; }
}

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

Now the code of entity remove operation could look like this :

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

实际上: ModifiedBy 更新将永远不会执行(当我的数据库历史记录触发器希望对其进行处理时)。 将仅在数据库上执行删除语句

In real: ModifiedBy update will be never executed (when my db history triggers expect to "handle" it). Only delete statement will be executed on DB.

我想知道如何强制EF Core更新已删除的实体/条目(该实现

I want to know how to force EF Core "update" deleted entities/entries (which implements the specific interface) if entity was modified.

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

Note: RowVersion adds additional complexity.

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

P.S. To put additional SaveContext call manually - of course is an option, but I would like to have a generic solution: many various updates and deletes, then one SaveContext do all analyzes.

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

To update those properties manually before SaveContext collecting var deletedEntries = entries.Where(e => e.State == EntityState.Deleted && isAuditable(e)) it is not an option since it can ruin EF Core locks order management and therefore provoke deadlocks.

最清晰的解决方案将是只保存一个SaveContext调用,但在 EF CORE 调用 DELETE 之前,在可审核字段上插入UPDATE语句。如何实现呢?

Most clear solution would be just stay with one SaveContext call but inject UPDATE statement on auditable fields just before EF CORE call DELETE. How to achieve this? May be somebody has the solution already?

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

Alternative could be "on delete do not compose DELETE statement but call stored procedure that can accept auditable fields as paramaters"

推荐答案


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

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。以下解决方案基于内部API,幸运的是,在EF Core中可以使用典型的内部API免责声明来公开访问这些内部API:

Interesting question. At the time of writing (EF Core 2.1.3), there is no such public API. The following solution is based on the internal APIs, which in EF Core fortunately are publicly accessible under the typical internal API disclaimer:


实体框架核心基础结构,不能直接在您的代码中使用。此API可能会在将来的发行版中更改或删除。

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

Now the solution. The service responsible for modification command creation is called ICommandBatchPreparer:


用于准备列表的服务 ModificationCommandBatch IUpdateEntry

它包含一个名为 BatchCommands

It contains a single method called BatchCommands:


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

具有以下签名:

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

CommandBatchPreparer 类。

我们将使用自定义实现替换该服务,这将使用修改过的条目扩展列表,并使用基本实现来完成实际工作。因为批处理基本上是一列修改命令的列表,这些列表按相关性排序,然后按类型排序,其中删除更新,我们将首先对更新命令使用单独的批处理,然后对其余命令进行连接。

We will replace that service with custom implementation which will extend the list with "modified" entries and use the base implementation to do the actual job. Since batch is basically a lists of modification commands sorted by dependency and then by type with Delete being before Update, we will use separate batch(es) for the update commands first and concatenate the rest after.

生成的修改命令基于 IUpdateEntry

The generated modification commands are based on IUpdateEntry:


传递给数据库提供商以将对实体的更改保存到数据库的信息。

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

幸运的是,它是一个接口,因此我们将为其他已修改条目及其相应的删除条目提供自己的实现(

Luckily it's an interface, so we will provide our own implementation for the additional "modified" entries, as well as for their corresponding delete entries (more on that later).

首先,我们将创建一个基本实现,该实现简单地将调用委派给基础对象,从而允许我们稍后进行覆盖。仅对我们要实现的目标必不可少的方法:

First we'll create a base implementation which simply delegates the calls to the underlying object, thus allowing us to override later only the methods that are essential for what we are trying to achieve:

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

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

Now the first custom entry:

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));
}

首先,我们从修改源状态已修改。然后,我们修改 IsModified 方法,该方法为已删除返回 false 条目为可审核属性返回 true ,从而强制将它们包含在update命令中。最后,我们修改 IsStoreGenerated 方法,该方法还会为已删除 false >条目以返回 Modified 条目的相应结果( EF核心代码)。为了使EF Core在更新时可以正确处理数据库生成的值,例如 RowVersion 。执行该命令后,EF Core将使用数据库返回的值调用 SetCurrentValue 。对于普通的 Deleted 条目和普通的 Modified 条目,这不会传播到它们的实体。

First we "modify" the source state from Deleted to Modified. Then we modify the IsModified method which returns false for Deleted entries to return true for the auditable properties, thus forcing them to be included in the update command. Finally we modify the IsStoreGenerated method which also returns false for Deleted entries to return the corresponding result for the Modified entries (EF Core code). This is needed to let EF Core correctly handle the database generated values on update like RowVersion. After executing the command, EF Core will call SetCurrentValue with the values returned from the database. Which does not happen for normal Deleted entries and for normal Modified entries propagates to their entity.

这将导致我们需要第二个自定义条目,该条目将包裹原始条目,还将用作 AuditUpdateEntry 的来源,因此将从其中接收 SetCurrentValue 。它将内部存储接收到的值,从而使原始实体状态保持不变,并将它们视为当前和原始。这是至关重要的,因为删除命令将在更新后执行,并且如果 RowVersion 不将新值返回为原始,则生成的删除命令将失败。

Which leads us to the need of the second custom entry, which will wrap the original entry and also will be used as source for the AuditUpdateEntry, hence will receive the SetCurrentValue from it. It will store the received values internally, thus keeping the original entity state unchanged, and will treat them as both "current" and "original". This is essential because the delete command will be executed after update, and if the RowVersion does not return the new value as "original", the generated delete command will fail.

这里是实现:

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;
    }
}

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

With these two custom entries we are ready to implement our custom command batch builder:

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);
    }
}

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

and we are almost done. Add a helper method for registering our service(s):

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

并致电给您 DbContext 派生类 OnConfiguring 覆盖:

and call it from you DbContext derived class OnConfiguring override:

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

,您就完成了。

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

All this is for single auditable field populated manually just to get the idea. It can be extended with more auditable fields, registering a custom auditable fields provider service and automatically filling the values for insert/update/delete operations etc.

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

这篇关于如何在SaveContext上更新已修改和已删除的实体?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆