我想知道如何在 EF 调用其“DELETE 语句”之前注入“UPDATE 语句”?我们有这样的API吗?
有趣的问题。在撰写本文时(EF Core 2.1.3),还没有这样的publicAPI。以下解决方案基于内部 API,幸运的是,在 EF Core 中,可以根据典型的内部 API 免责声明公开访问这些 API:
此 API 支持 Entity Framework Core 基础设施,并不适合直接在您的代码中使用。此 API 可能会在未来版本中更改或删除。
现在解决方案。负责创建修改命令的服务称为ICommandBatchPreparer https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.update.icommandbatchpreparer?view=efcore-2.1:
准备清单的服务修改命令批处理 https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.update.modificationcommandbatch?view=efcore-2.1s 表示由给定列表表示的实体更新条目 https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.update.iupdateentry?view=efcore-2.1s.
它包含一个名为BatchCommands https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.update.icommandbatchpreparer.batchcommands?view=efcore-2.1:
创建插入/更新/删除给定列表表示的实体所需的命令批处理更新条目 https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.update.iupdateentry?view=efcore-2.1s.
带有以下签名:
public IEnumerable<ModificationCommandBatch> BatchCommands(
IReadOnlyList<IUpdateEntry> entries);
和默认实现CommandBatchPreparer https://github.com/aspnet/EntityFrameworkCore/blob/release/2.2/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs class.
我们将用自定义实现替换该服务,这将用“修改的”条目扩展列表,并使用基本实现来完成实际工作。由于批处理基本上是按依赖关系排序的修改命令列表,然后by type with Delete
之前Update
,我们将首先对更新命令使用单独的批次,然后连接其余的命令。
生成的修改命令基于IUpdateEntry https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.update.iupdateentry?view=efcore-2.1:
传递给数据库提供程序以将对实体所做的更改保存到数据库的信息。
幸运的是,它是一个接口,因此我们将为附加的“修改”条目及其相应的删除条目提供我们自己的实现(稍后会详细介绍)。
首先,我们将创建一个基本实现,它只是将调用委托给底层对象,从而允许我们稍后仅重写对于我们想要实现的目标至关重要的方法:
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
to Modified
。然后我们修改IsModified
返回的方法false
for Deleted
要返回的条目true
用于可审核属性,从而强制将它们包含在更新命令中。最后我们修改IsStoreGenerated
方法也返回false
for Deleted
条目返回相应的结果Modified
条目(EF 核心代码 https://github.com/aspnet/EntityFrameworkCore/blob/release/2.2/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs#L1146)。这是让 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;
}
}
有了这两个自定义条目,我们就可以实现自定义命令批处理生成器了:
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
派生类OnConfiguring
覆盖:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// ...
optionsBuilder.AddAudit();
}
你就完成了。
所有这些都是为了手动填充单个可审核字段以了解想法。它可以扩展更多可审计字段,注册自定义可审计字段提供程序服务并自动填充插入/更新/删除操作等的值。
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();
}
}
Update: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();
}
}