前言

数据库并发,数据审计和软删除一直是数据持久化方面的经典问题。早些时候,这些工作需要手写复杂的SQL或者通过存储过程和触发器实现。手写复杂SQL对软件可维护性构成了相当大的挑战,随着SQL字数的变多,用到的嵌套和复杂语法增加,可读性和可维护性的难度是几何级暴涨。因此如何在实现功能的同时控制这些SQL的复杂度是一个很有价值的问题。而且这个问题同时涉及应用软件和数据库两个相对独立的体系,平行共管也是产生混乱的一大因素。

EF Core作为 .NET平台的高级ORM框架,可以托管和数据库的交互,同时提供了大量扩展点方便自定义。以此为基点把对数据库的操作托管后便可以解决平行共管所产生的混乱,利用LINQ则可以最大程度上降低软件代码的维护难度。

由于项目需要,笔者先后开发并发布了通用的基于EF Core的国际化资源管理服务和Serilog日志持久化服务,不过这两个功能包并没有深度利用EF Core,虽然主要是因为没什么必要。但是项目还需要提供常用的数据审计和软删除功能,因此对EF Core进行了一些更深入的研究。

起初有考虑过是否使用现成的ABP框架来处理这些功能,但是在其他项目的使用体验来说并不算好,其中充斥着大量上下文依赖的功能,而且这些依赖信息能轻易藏到和最终业务代码相距十万八千里的地方(特别是代码还是别人写的时候),然后在不经意间给你一个大惊喜。对于以代码正交性、非误导性,纯函数化为追求的一介码农(看过我发布的那两个功能包的朋友应该有感觉,一个功能笔者也要根据用途划分为不同的包,确保解决方案中的各个项目都能按需引用,不会残留无用的代码),实在是喜欢不起来ABP这种全家桶。

鉴于项目规模不大,笔者决定针对这些需求做一个专用功能,目标是尽可能减少依赖,方便将来复用到其他项目,降低和其他功能功能冲突的风险。现在笔者将用一系列博客做成果展示。由于这些功能没有经过大范围测试,不确定是否存在未知缺陷,因此暂不打包发布。

新书宣传

有关新书的更多介绍欢迎查看《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!

正文

由于这些功能设计的代码量和知识点较多,为控制篇幅,本文介绍数据审计和乐观并发功能。

EF Core 3.0新增了侦听器功能,允许在实际执行操作之前或之后插入自定义操作,利用这个功能可以实现数据审计的自动化。为此需要做些前期准备。

审计实体接口

乐观并发接口

/// <summary>
/// 乐观并发接口
/// </summary>
public interface IOptimisticConcurrencySupported
{
/// <summary>
/// 行版本,乐观并发锁
/// </summary>
[ConcurrencyCheck]
string? ConcurrencyStamp { get; set; }
}

SqlServer数据库支持自动的行版本功能,但是大多数其他数据库并不支持,因此选用兼容性更好的方案。Identity Core为了兼容性也不用行版本实现乐观并发。

时间审计接口

/// <summary>
/// 创建和最近更新时间审计的合成接口
/// </summary>
public interface IFullyTimeAuditable : ICreationTimeAuditable, ILastUpdateTimeAuditable; /// <summary>
/// 创建时间审计接口
/// </summary>
public interface ICreationTimeAuditable
{
/// <summary>
/// 创建时间标记
/// </summary>
DateTimeOffset? CreatedAt { get; set; }
} /// <summary>
/// 最近更新时间审计接口
/// </summary>
public interface ILastUpdateTimeAuditable
{
/// <summary>
/// 最近更新时间标记
/// </summary>
DateTimeOffset? LastUpdatedAt { get; set; }
}

操作人审计接口

/// <summary>
/// 创建和最近更新用户审计的合成接口
/// </summary>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
public interface IFullyOperatorAuditable<TIdentityKey>
: ICreationUserAuditable<TIdentityKey>
, ILastUpdateUserAuditable<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>; /// <summary>
/// 包括导航的创建和最近更新用户审计的合成接口
/// </summary>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <typeparam name="TUser">用户类型</typeparam>
public interface IFullyOperatorAuditable<TIdentityKey, TUser>
: ICreationUserAuditable<TIdentityKey, TUser>
, ILastUpdateUserAuditable<TIdentityKey, TUser>
where TIdentityKey : struct, IEquatable<TIdentityKey>
where TUser : class; /// <summary>
/// 创建用户审计接口
/// </summary>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
public interface ICreationUserAuditable<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
/// <summary>
/// 创建用户Id
/// </summary>
TIdentityKey? CreatedById { get; set; }
} /// <summary>
/// 包括导航的创建用户审计接口
/// </summary>
/// <typeparam name="TUser">用户类型</typeparam>
/// <inheritdoc />
public interface ICreationUserAuditable<TIdentityKey, TUser> : ICreationUserAuditable<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>
where TUser : class
{
/// <summary>
/// 创建用户
/// </summary>
TUser? CreatedBy { get; set; }
} /// <summary>
/// 最近更新用户审计接口
/// </summary>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
public interface ILastUpdateUserAuditable<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
/// <summary>
/// 最近更新用户Id
/// </summary>
TIdentityKey? LastUpdatedById { get; set; }
} /// <summary>
/// 包括导航的最近更新用户审计接口
/// </summary>
/// <typeparam name="TUser">用户类型</typeparam>
/// <inheritdoc />
public interface ILastUpdateUserAuditable<TIdentityKey, TUser> : ILastUpdateUserAuditable<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>
where TUser : class
{
/// <summary>
/// 最近更新用户
/// </summary>
TUser? LastUpdatedBy { get; set; }
}

使用接口方便和已有代码集成。带导航的操作人接口使用结构体Id方便准确控制外键可空性。

需要的辅助方法

public static class RuntimeTypeExtensions
{
/// <summary>
/// 判断 <paramref name="type"/> 指定的类型是否派生自 <typeparamref name="T"/> 类型,或实现了 <typeparamref name="T"/> 接口
/// </summary>
/// <typeparam name="T">要匹配的类型</typeparam>
/// <param name="type">需要测试的类型</param>
/// <returns>如果 <paramref name="type"/> 指定的类型派生自 <typeparamref name="T"/> 类型,或实现了 <typeparamref name="T"/> 接口,则返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
public static bool IsDerivedFrom<T>(this Type type)
{
return IsDerivedFrom(type, typeof(T));
} /// <summary>
/// 判断 <paramref name="type"/> 指定的类型是否继承自 <paramref name="pattern"/> 指定的类型,或实现了 <paramref name="pattern"/> 指定的接口
/// <para>支持开放式泛型,如<see cref="List{T}" /></para>
/// </summary>
/// <param name="type">需要测试的类型</param>
/// <param name="pattern">要匹配的类型,如 <c>typeof(int)</c>,<c>typeof(IEnumerable)</c>,<c>typeof(List&lt;&gt;)</c>,<c>typeof(List&lt;int&gt;)</c>,<c>typeof(IDictionary&lt;,&gt;)</c></param>
/// <returns>如果 <paramref name="type"/> 指定的类型继承自 <paramref name="pattern"/> 指定的类型,或实现了 <paramref name="pattern"/> 指定的接口,则返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
public static bool IsDerivedFrom(this Type type, Type pattern)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(pattern); // 测试非泛型类型(如ArrayList)或确定类型参数的泛型类型(如List<int>,类型参数T已经确定为 int)
if (type.IsSubclassOf(pattern)) return true; // 测试非泛型接口(如IEnumerable)或确定类型参数的泛型接口(如IEnumerable<int>,类型参数T已经确定为 int)
if (pattern.IsAssignableFrom(type)) return true; // 测试泛型接口(如IEnumerable<>,IDictionary<,>,未知类型参数,留空)
var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType);
if (isTheRawGenericType) return true; // 测试泛型类型(如List<>,Dictionary<,>,未知类型参数,留空)
while (type != null && type != typeof(object))
{
isTheRawGenericType = IsTheRawGenericType(type);
if (isTheRawGenericType) return true;
type = type.BaseType!;
} // 没有找到任何匹配的接口或类型。
return false; // 测试某个类型是否是指定的原始接口。
bool IsTheRawGenericType(Type test)
=> pattern == (test.IsGenericType ? test.GetGenericTypeDefinition() : test);
}
} /// <summary>
/// 实体配置相关泛型方法生成扩展
/// </summary>
internal static class EntityConfigurationMethodsHelper
{
private const BindingFlags _bindingFlags = BindingFlags.Public | BindingFlags.Static;
private static readonly ImmutableArray<MethodInfo> _configurationMethods;
private static readonly MethodInfo _genericEntityTypeBuilderGetterMethod; static EntityConfigurationMethodsHelper()
{
_configurationMethods =
[
.. typeof(EntityModelBuilderExtensions).GetMethods(_bindingFlags),
.. typeof(OperationUserAuditableEntityModelBuilderExtensions).GetMethods(_bindingFlags),
.. typeof(TimeAuditableEntityModelBuilderExtensions).GetMethods(_bindingFlags),
.. typeof(TreeEntityModelBuilderExtensions).GetMethods(_bindingFlags),
]; _genericEntityTypeBuilderGetterMethod = typeof(ModelBuilder)
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(static m => m.Name is nameof(ModelBuilder.Entity))
.Where(static m => m.IsGenericMethod)
.Where(static m => m.GetParameters().Length is 0)
.Single();
} /// <summary>
/// 获取泛型实体类型配置扩展方法
/// </summary>
/// <param name="name">方法名</param>
/// <param name="ParametersCount">参数数量</param>
/// <returns>已生成的封闭式泛型配置扩展方法</returns>
internal static MethodInfo GetEntityTypeConfigurationMethod(string name, int ParametersCount, params Type[] typeParameterTypes)
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(typeParameterTypes); return _configurationMethods
.Where(m => m.Name == name)
.Where(m => m.GetParameters().Length == ParametersCount)
.Where(static m => m.IsGenericMethod)
.Where(m => m.GetGenericArguments().Length == typeParameterTypes.Length)
.Single()
.MakeGenericMethod(typeParameterTypes); } /// <summary>
/// 获取泛型实体类型构造器
/// </summary>
/// <param name="entity">实体类型</param>
/// <returns></returns>
internal static MethodInfo GetEntityTypeBuilderMethod(IMutableEntityType entity)
{
ArgumentNullException.ThrowIfNull(entity); // 动态生成泛型方法使配置逻辑拥有唯一的定义位置,避免发生不必要的问题
return _genericEntityTypeBuilderGetterMethod.MakeGenericMethod(entity.ClrType);
}
} /// <summary>
/// 指示实体配置适用于何种数据库提供程序
/// </summary>
/// <param name="ProviderName"></param>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class DatabaseProviderAttribute(string ProviderName) : Attribute
{
/// <summary>
/// 提供程序名称
/// </summary>
public string ProviderName { get; } = ProviderName;
}

把实体配置扩展方法缓存起来方便之后批量调用,因为EF Core的泛型和非泛型实体构造器无法直接转换,只能通过反射动态生成泛型方法复用单体配置扩展。这样能保证配置代码只有唯一一份,避免重复代码导致维护时出现疏漏。

实体模型配置扩展

乐观并发扩展

/// <summary>
/// 配置乐观并发实体的并发检查字段
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="builder">实体类型构造器</param>
/// <returns>实体属性构造器</returns>
public static PropertyBuilder<string> ConfigureForIOptimisticConcurrencySupported<TEntity>(
this EntityTypeBuilder<TEntity> builder)
where TEntity : class, IOptimisticConcurrencySupported
{
ArgumentNullException.ThrowIfNull(builder); return builder.Property(e => e.ConcurrencyStamp!).IsConcurrencyToken();
} /// <summary>
/// 批量配置乐观并发实体的并发检查字段
/// </summary>
/// <param name="modelBuilder">模型构造器</param>
/// <returns>模型构造器</returns>
public static ModelBuilder ConfigureForIOptimisticConcurrencySupported(this ModelBuilder modelBuilder)
{
ArgumentNullException.ThrowIfNull(modelBuilder); foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<IOptimisticConcurrencySupported>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
var optimisticConcurrencySupportedMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForIOptimisticConcurrencySupported),
1,
entity.ClrType); optimisticConcurrencySupportedMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
} return modelBuilder;
}

时间审计扩展

/// <summary>
/// 实体时间审计配置扩展
/// </summary>
public static class TimeAuditableEntityModelBuilderExtensions
{
/// <summary>
/// 配置创建时间审计
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="builder">实体类型构造器</param>
/// <param name="defaultValueSql">默认值Sql</param>
/// <returns>实体类型构造器</returns>
public static EntityTypeBuilder<TEntity> ConfigureForCreationTimeAuditable<TEntity>(
this EntityTypeBuilder<TEntity> builder,
ITimeAuditableDefaultValueSql defaultValueSql)
where TEntity : class, ICreationTimeAuditable
{
builder.Property(e => e.CreatedAt)
.IsRequired()
.HasDefaultValueSql(defaultValueSql.Sql); return builder;
} /// <summary>
/// 批量配置创建时间审计
/// </summary>
/// <param name="modelBuilder">模型构造器</param>
/// <returns>模型构造器</returns>
public static ModelBuilder ConfigureForCreationTimeAuditable(
this ModelBuilder modelBuilder,
ITimeAuditableDefaultValueSql defaultValueSql)
{
foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<ICreationTimeAuditable>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
var creationTimeAuditableMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForCreationTimeAuditable),
2,
entity.ClrType); creationTimeAuditableMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null), defaultValueSql]);
} return modelBuilder;
} /// <summary>
/// 配置最近更新时间审计
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="builder">实体类型构造器</param>
/// <param name="defaultValueSql">默认值Sql</param>
/// <returns>实体类型构造器</returns>
public static EntityTypeBuilder<TEntity> ConfigureForLastUpdateTimeAuditable<TEntity>(
this EntityTypeBuilder<TEntity> builder,
ITimeAuditableDefaultValueSql defaultValueSql)
where TEntity : class, ILastUpdateTimeAuditable
{
builder.Property(e => e.LastUpdatedAt)
.IsRequired()
.HasDefaultValueSql(defaultValueSql.Sql); return builder;
} /// <summary>
/// 批量配置最近更新时间审计
/// </summary>
/// <param name="modelBuilder">模型构造器</param>
/// <returns>模型构造器</returns>
public static ModelBuilder ConfigureForLastUpdateTimeAuditable(
this ModelBuilder modelBuilder,
ITimeAuditableDefaultValueSql defaultValueSql)
{
foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<ILastUpdateTimeAuditable>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
var lastUpdateTimeAuditableMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForLastUpdateTimeAuditable),
2,
entity.ClrType); lastUpdateTimeAuditableMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null), defaultValueSql]);
} return modelBuilder;
} /// <summary>
/// 配置完整时间审计
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="builder">实体类型构造器</param>
/// <param name="defaultValueSql">默认值Sql</param>
/// <returns>实体类型构造器</returns>
public static EntityTypeBuilder<TEntity> ConfigureForFullyTimeAuditable<TEntity>(
this EntityTypeBuilder<TEntity> builder,
ITimeAuditableDefaultValueSql defaultValueSql)
where TEntity : class, IFullyTimeAuditable
{
builder
.ConfigureForCreationTimeAuditable(defaultValueSql)
.ConfigureForLastUpdateTimeAuditable(defaultValueSql); return builder;
} /// <summary>
/// 批量配置时间审计
/// </summary>
/// <param name="modelBuilder">模型构造器</param>
/// <returns>模型构造器</returns>
public static ModelBuilder ConfigureForTimeAuditable(
this ModelBuilder modelBuilder,
ITimeAuditableDefaultValueSql defaultValueSql)
{
modelBuilder
.ConfigureForCreationTimeAuditable(defaultValueSql)
.ConfigureForLastUpdateTimeAuditable(defaultValueSql); return modelBuilder;
}
}

时间审计使用默认值SQL尽可能使数据库和代码统一逻辑,即使直接向数据库插入记录也能尽量保证有相关审计数据。只是最近更新时间在更新时实在是做不到数据库级别的自动,用触发器会阻止手动操作数据,所以不用。

时间列的默认值SQL在不同数据库下有差异,因此需要从外部传入,方便根据数据库类型切换。

/// <summary>
/// 实体时间审计默认值Sql
/// </summary>
public interface ITimeAuditableDefaultValueSql
{
string Sql { get; }
} public class DefaultSqlServerTimeAuditableDefaultValueSql : ITimeAuditableDefaultValueSql
{
public static DefaultSqlServerTimeAuditableDefaultValueSql Instance => new(); public string Sql => "GETDATE()"; private DefaultSqlServerTimeAuditableDefaultValueSql() { }
} public class DefaultMySqlTimeAuditableDefaultValueSql : ITimeAuditableDefaultValueSql
{
public static DefaultMySqlTimeAuditableDefaultValueSql Instance => new(); public string Sql => "CURRENT_TIMESTAMP(6)"; private DefaultMySqlTimeAuditableDefaultValueSql() { }
}

操作人审计扩展

/// <summary>
/// 实体操作人审计配置扩展
/// </summary>
public static class OperationUserAuditableEntityModelBuilderExtensions
{
/// <summary>
/// 配置实体创建人外键和导航属性
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <param name="builder">实体类型构造器</param>
/// <returns>实体类型构造器</returns>
public static EntityTypeBuilder<TEntity> ConfigureForNavigationIncludedCreationUserAuditable<TEntity, TUser, TIdentityKey>(
this EntityTypeBuilder<TEntity> builder)
where TEntity : class, ICreationUserAuditable<TIdentityKey, TUser>
where TUser : class
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
builder
.HasOne(b => b.CreatedBy)
.WithMany()
.HasForeignKey(b => b.CreatedById); return builder;
} /// <summary>
/// 批量配置实体创建人外键和导航属性
/// </summary>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <param name="modelBuilder">实体构造器</param>
/// <returns>当前实体构造器</returns>
public static ModelBuilder ConfigureForNavigationIncludedCreationUserAuditable<TUser, TIdentityKey>(
this ModelBuilder modelBuilder)
where TUser : class
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<ICreationUserAuditable<TIdentityKey, TUser>>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
var navigationIncludedCreationUserAuditableMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForNavigationIncludedCreationUserAuditable),
1,
[entity.ClrType, typeof(TUser), typeof(TIdentityKey)]); navigationIncludedCreationUserAuditableMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
} return modelBuilder;
} /// <summary>
/// 批量配置实体创建人外键,如果有导航属性就同时配置导航属性
/// </summary>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <param name="modelBuilder">实体构造器</param>
/// <returns>当前实体构造器</returns>
public static ModelBuilder ConfigureForCreationUserOrNavigationIncludedAuditable<TUser, TIdentityKey>(
this ModelBuilder modelBuilder)
where TUser : class
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<ICreationUserAuditable<TIdentityKey>>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity); MethodInfo creationUserAuditableMethod;
if (entity.ClrType.IsDerivedFrom<ICreationUserAuditable<TIdentityKey, TUser>>())
{
creationUserAuditableMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForNavigationIncludedCreationUserAuditable),
1,
[entity.ClrType, typeof(TUser), typeof(TIdentityKey)]); creationUserAuditableMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
}
} return modelBuilder;
} /// <summary>
/// 配置实体最近修改人外键和导航属性
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <param name="builder">实体类型构造器</param>
/// <returns>实体类型构造器</returns>
public static EntityTypeBuilder<TEntity> ConfigureForNavigationIncludedLastUpdateUserAuditable<TEntity, TUser, TIdentityKey>(
this EntityTypeBuilder<TEntity> builder)
where TEntity : class, ILastUpdateUserAuditable<TIdentityKey, TUser>
where TUser : class
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
builder
.HasOne(b => b.LastUpdatedBy)
.WithMany()
.HasForeignKey(b => b.LastUpdatedById); return builder;
} /// <summary>
/// 批量配置实体最近修改人外键和导航属性
/// </summary>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <param name="modelBuilder">实体构造器</param>
/// <returns>当前实体构造器</returns>
public static ModelBuilder ConfigureForNavigationIncludedLastUpdateUserAuditable<TUser, TIdentityKey>(
this ModelBuilder modelBuilder)
where TUser : class
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<ILastUpdateUserAuditable<TIdentityKey, TUser>>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
var navigationIncludedLastUpdateUserAuditableMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForNavigationIncludedLastUpdateUserAuditable),
1,
[entity.ClrType, typeof(TUser), typeof(TIdentityKey)]); navigationIncludedLastUpdateUserAuditableMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
} return modelBuilder;
} /// <summary>
/// 批量配置实体最近修改人外键,如果有导航属性就同时配置导航属性
/// </summary>
/// <typeparam name="TUser">用户实体类型</typeparam>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
/// <param name="modelBuilder">实体构造器</param>
/// <returns>当前实体构造器</returns>
public static ModelBuilder ConfigureForLastUpdateUserOrNavigationIncludedAuditable<TUser, TIdentityKey>(
this ModelBuilder modelBuilder)
where TUser : class
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
foreach (var entity
in modelBuilder.Model.GetEntityTypes()
.Where(static e => !e.HasSharedClrType)
.Where(static e => e.ClrType.IsDerivedFrom<ILastUpdateUserAuditable<TIdentityKey>>()))
{
var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity); MethodInfo lastUpdateUserAuditableMethod;
if (entity.ClrType.IsDerivedFrom<ILastUpdateUserAuditable<TIdentityKey, TUser>>())
{
lastUpdateUserAuditableMethod = GetEntityTypeConfigurationMethod(
nameof(ConfigureForNavigationIncludedLastUpdateUserAuditable),
1,
[entity.ClrType, typeof(TUser), typeof(TIdentityKey)]); lastUpdateUserAuditableMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null)]);
}
} return modelBuilder;
}
}

没有导航属性的接口是为用户表在其他数据库的情况预留的,因此这个版本的接口不做作任何特殊配置。

数据库上下文

// 其中IdentityKey是int的全局类型别名,上下文类型继承自Identity Core上下文,用于演示操作用户自动审计
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: ApplicationIdentityDbContext<
ApplicationUser,
ApplicationRole,
IdentityKey,
ApplicationUserClaim,
ApplicationUserRole,
ApplicationUserLogin,
ApplicationRoleClaim,
ApplicationUserToken>(options)
{
// 其他无关代码 protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder); // 其他无关代码 // 自动根据数据库类型进行数据库相关的模型配置
switch (Database.ProviderName)
{
case _msSqlServerProvider:
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(LogRecordEntityTypeConfiguration).Assembly,
type => type.GetCustomAttributes<DatabaseProviderAttribute>().Any(a => a.ProviderName is _msSqlServerProvider)); modelBuilder.ConfigureForTimeAuditable(DefaultSqlServerTimeAuditableDefaultValueSql.Instance);
break;
case _pomeloMySqlProvider:
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(LogRecordEntityTypeConfiguration).Assembly,
type => type.GetCustomAttributes<DatabaseProviderAttribute>().Any(a => a.ProviderName is _pomeloMySqlProvider)); modelBuilder.ConfigureForTimeAuditable(DefaultMySqlTimeAuditableDefaultValueSql.Instance);
break;
case _msSqliteProvider:
goto default;
default:
throw new NotSupportedException(Database.ProviderName);
} // 配置其他数据库中立的模型配置
modelBuilder.ConfigureForIOptimisticConcurrencySupported(); modelBuilder.ConfigureForCreationUserOrNavigationIncludedAuditable<ApplicationUser, IdentityKey>();
modelBuilder.ConfigureForLastUpdateUserOrNavigationIncludedAuditable<ApplicationUser, IdentityKey>();
}
}

项目使用MySQL,而VS会附带一个SqlServer单机版,所以暂时使用这两个数据库进行演示,如果需要支持其他数据库,可自行改造。

EF Core侦听器

并发检查侦听器

/// <summary>
/// 为并发检查标记设置值,如果有逻辑删除实体,应该位于逻辑删除拦截器之后
/// </summary>
public class OptimisticConcurrencySupportedSaveChangesInterceptor : SaveChangesInterceptor
{
protected IServiceScopeFactory ScopeFactory { get; } public OptimisticConcurrencySupportedSaveChangesInterceptor(IServiceScopeFactory scopeFactory)
{
ArgumentNullException.ThrowIfNull(scopeFactory); ScopeFactory = scopeFactory;
} public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
OnSavingChanges(eventData);
return base.SavingChanges(eventData, result);
} public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
OnSavingChanges(eventData);
return base.SavingChangesAsync(eventData, result, cancellationToken);
} /// <summary>
/// 处理实体的并发检查令牌,并忽略由<see cref="ShouldProcessEntry"/>排除的实体
/// </summary>
/// <param name="eventData"></param>
protected virtual void OnSavingChanges(DbContextEventData eventData)
{
ArgumentNullException.ThrowIfNull(eventData.Context); eventData.Context.ChangeTracker.DetectChanges(); var entries = eventData.Context.ChangeTracker.Entries()
.Where(static e => e.State is EntityState.Added or EntityState.Modified)
.Where(ShouldProcessEntry); foreach (var entry in entries)
{
if (entry.Entity is IOptimisticConcurrencySupported optimistic)
{
if (entry.State is EntityState.Added)
{
optimistic.ConcurrencyStamp = Guid.NewGuid().ToString();
}
if (entry.State is EntityState.Modified)
{
// 如果是更新实体,需要分别处理原值和新值
var concurrencyStamp = entry.Property(nameof(IOptimisticConcurrencySupported.ConcurrencyStamp));
// 实体的当前值要指定为原值
concurrencyStamp!.OriginalValue = (entry.Entity as IOptimisticConcurrencySupported)!.ConcurrencyStamp;
// 然后重新生成新值
concurrencyStamp.CurrentValue = Guid.NewGuid().ToString();
}
}
}
} /// <summary>
/// 用于排除在其他位置处理过并发检查令牌的实体
/// </summary>
/// <param name="entry">实体</param>
/// <returns>如果应该由当前拦截器处理返回<see langword="true"/>,否则返回<see langword="false"/>。</returns>
protected virtual bool ShouldProcessEntry(EntityEntry entry) => true;
} /// <summary><inheritdoc cref="OptimisticConcurrencySupportedSaveChangesInterceptor"/></summary>
/// <remarks>忽略用户实体的并发检查令牌,Identity服务已经处理过实体</remarks>
public class IdentityOptimisticConcurrencySupportedSaveChangesInterceptor(IServiceScopeFactory scopeFactory)
: OptimisticConcurrencySupportedSaveChangesInterceptor(scopeFactory)
{
/// <summary>
/// 忽略Identity内置并发检查的实体
/// </summary>
/// <param name="entry">待检查的实体</param>
/// <returns>不是IdentityUser的实体</returns>
protected override bool ShouldProcessEntry(EntityEntry entry)
{
var type = entry.Entity.GetType();
var isUserOrRole = type.IsDerivedFrom(typeof(IdentityUser<>)) || type.IsDerivedFrom(typeof(IdentityRole<>));
return !isUserOrRole;
}
}

Identity Core有一套内置的并发检查处理机制,因此需要对Identity相关实体进行排除,防止重复处理引起异常。

时间审计侦听器

/// <summary>
/// 为操作时间审计设置值,如果已经手动设置有效值,不会再次设置。如果有逻辑删除实体,应该位于逻辑删除拦截器之前。<br/>
/// 删除时间已经由逻辑删除标记保留,不应该用删除时间覆盖更新时间,在逻辑删除之前使用避免误操作由逻辑删除拦截器设置的已编辑的实体。
/// </summary>
public class OperationTimeAuditableSaveChangesInterceptor : SaveChangesInterceptor
{
protected IServiceScopeFactory ScopeFactory { get; } /// <summary>
/// 为操作时间审计设置值,如果已经手动设置有效值,不会再次设置。如果有逻辑删除实体,应该位于逻辑删除拦截器之前。<br/>
/// 删除时间已经由逻辑删除标记保留,不应该用删除时间覆盖更新时间,在逻辑删除之前使用避免误操作由逻辑删除拦截器设置的已编辑的实体。
/// </summary>
/// <param name="scopeFactory"></param>
public OperationTimeAuditableSaveChangesInterceptor(IServiceScopeFactory scopeFactory)
{
ArgumentNullException.ThrowIfNull(scopeFactory); ScopeFactory = scopeFactory;
} public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
OnSavingChanges(eventData);
return base.SavingChanges(eventData, result);
} public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
OnSavingChanges(eventData);
return base.SavingChangesAsync(eventData, result, cancellationToken);
} /// <summary>
/// 处理实体的审计时间
/// </summary>
/// <param name="eventData"></param>
protected virtual void OnSavingChanges(DbContextEventData eventData)
{
ArgumentNullException.ThrowIfNull(eventData.Context); using var scope = ScopeFactory.CreateScope();
var timeProvider = scope.ServiceProvider.GetRequiredService<TimeProvider>(); eventData.Context.ChangeTracker.DetectChanges(); var entries = eventData.Context.ChangeTracker.Entries()
.Where(static e => e.State is EntityState.Added or EntityState.Modified); foreach (var entry in entries)
{
if(entry is { Entity: ICreationTimeAuditable creation, State: EntityState.Added })
{
if(creation.CreatedAt is null || creation.CreatedAt == default)
{
creation.CreatedAt = timeProvider.GetLocalNow();
}
} if (entry is { Entity: ILastUpdateTimeAuditable update, State: EntityState.Added or EntityState.Modified })
{
if (entry.Property(nameof(update.LastUpdatedAt)).IsModified) { }
else if (update.LastUpdatedAt is null || update.LastUpdatedAt == default)
{
update.LastUpdatedAt = timeProvider.GetLocalNow();
} if (entry is { Entity: ICreationTimeAuditable, State: EntityState.Modified })
{
entry.Property(nameof(ICreationTimeAuditable.CreatedAt)).IsModified = false;
}
}
}
}
}

操作人审计侦听器

/// <summary>
/// 为操作人审计设置值,如果已经手动设置有效值,不会再次设置。如果有逻辑删除实体,应该位于逻辑删除拦截器之后。<br/>
/// 到此处依然处于删除状态的实体应该是物理删除,记录审计信息没有意义。
/// </summary>
public class OperatorAuditableSaveChangesInterceptor<TIdentityKey> : SaveChangesInterceptor
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
protected IServiceScopeFactory ScopeFactory { get; } /// <summary>
/// 为操作人审计设置值,如果已经手动设置有效值,不会再次设置。如果有逻辑删除实体,应该位于逻辑删除拦截器之后。<br/>
/// 到此处依然处于删除状态的实体应该是物理删除,记录审计信息没有意义。
/// </summary>
/// <param name="scopeFactory"></param>
public OperatorAuditableSaveChangesInterceptor(IServiceScopeFactory scopeFactory)
{
ArgumentNullException.ThrowIfNull(scopeFactory); ScopeFactory = scopeFactory;
} public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
OnSavingChanges(eventData);
return base.SavingChanges(eventData, result);
} public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
OnSavingChanges(eventData);
return base.SavingChangesAsync(eventData, result, cancellationToken);
} /// <summary>
/// 处理实体的审计操作人
/// </summary>
/// <param name="eventData"></param>
protected virtual void OnSavingChanges(DbContextEventData eventData)
{
ArgumentNullException.ThrowIfNull(eventData.Context); using var scope = ScopeFactory.CreateScope();
var operatorAccessor = scope.ServiceProvider.GetRequiredService<IOperatorAccessor<TIdentityKey>>(); eventData.Context.ChangeTracker.DetectChanges(); var entries = eventData.Context.ChangeTracker.Entries()
.Where(static e => e.State is EntityState.Added or EntityState.Modified); foreach (var entry in entries)
{
if (entry is { Entity: ICreationUserAuditable<TIdentityKey> creation, State: EntityState.Added })
{
if (creation.CreatedById is null || creation.CreatedById.Value.Equals(default))
{
creation.CreatedById = operatorAccessor.GetUserId();
}
} if (entry is { Entity: ILastUpdateUserAuditable<TIdentityKey> update, State: EntityState.Added or EntityState.Modified })
{
if (entry.Property(nameof(update.LastUpdatedById)).IsModified) { }
else if (update.LastUpdatedById is null || update.LastUpdatedById.Value.Equals(default))
{
update.LastUpdatedById = operatorAccessor.GetUserId();
} if (entry is { Entity: ICreationUserAuditable<TIdentityKey>, State: EntityState.Modified })
{
entry.Property(nameof(ICreationUserAuditable<TIdentityKey>.CreatedById)).IsModified = false;
}
}
}
}
} /// <summary>
/// 实体操作人的用户Id提供服务
/// </summary>
/// <typeparam name="TIdentityKey">用户Id类型</typeparam>
public interface IOperatorAccessor<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>
{
/// <summary>
/// 获取用户Id
/// </summary>
/// <returns>用户Id</returns>
TIdentityKey? GetUserId(); /// <summary>
/// 异步获取用户Id
/// </summary>
/// <param name="cancellation">取消令牌</param>
/// <returns>用户Id</returns>
Task<TIdentityKey?> GetUserIdAsync(CancellationToken cancellation = default);
} /// <summary>
/// 使用Http上下文获取实体操作人的用户Id
/// </summary>
/// <typeparam name="TIdentityKey"><inheritdoc cref="IOperatorAccessor{TIdentityKey}"/></typeparam>
/// <param name="contextAccessor">Http上下文访问器</param>
/// <param name="options">Identity选项</param>
public class HttpContextUserOperatorAccessor<TIdentityKey>(
IHttpContextAccessor contextAccessor,
IOptions<IdentityOptions> options)
: IOperatorAccessor<TIdentityKey>
where TIdentityKey : struct, IEquatable<TIdentityKey>, IParsable<TIdentityKey>
{
public TIdentityKey? GetUserId()
{
var success = TIdentityKey.TryParse(contextAccessor.HttpContext?.User.Claims.FirstOrDefault(c => c.Type == options.Value.ClaimsIdentity.UserIdClaimType)!.Value, null, out var id);
return success ? id : null;
} public Task<TIdentityKey?> GetUserIdAsync(CancellationToken cancellation = default)
{
return Task.FromResult(GetUserId());
}
}

实体操作人的获取在定义侦听器的时候是未知的,所以获取方式需要通过接口从外部传入。此处以用ASP.NET Core Identity获取用户Id为例。

侦听器统一使用作用域工厂服务使其能和依赖注入系统紧密配合,然后使用内部作用域即用即取,用完立即销毁的方式避免内存泄露。

配置服务

一切准备妥当后就可以在主应用里配置相关服务让功能可以正常运行。

public void ConfigureServices(IServiceCollection services)
{
// 实体操作人审计EF Core拦截器需要使用此服务获取操作人信息
services.AddScoped(typeof(IOperatorAccessor<>), typeof(HttpContextUserOperatorAccessor<>)); // 注册基于缓冲池的数据库上下文工厂
services.AddPooledDbContextFactory<ApplicationDbContext>((sp, options) =>
{
// 注册拦截器
var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
options.AddInterceptors(
new OperationTimeAuditableSaveChangesInterceptor(scopeFactory),
new IdentityOptimisticConcurrencySupportedSaveChangesInterceptor(scopeFactory),
new OperatorAuditableSaveChangesInterceptor<IdentityKey>(scopeFactory)); // 其它代码
}); // 其它代码
}

由于拦截器对象是长期存在且脱离依赖注入的特殊对象,因此需要从外部传入作用域工厂使其能够使用依赖注入的相关功能和整个ASP.NET Core应用更紧密的集成。拦截器和ASP.NET Core中间件一样顺序会影响结果,因此要认真考虑如何安排。

结语

如此一番操作之后,操作时间、操作用户审计和乐观并发就全自动化了,一般业务代码可以0修改完成集成。如果手动操作相关属性,侦听器也会优先采用手动操作的结果保持充足的灵活性。

示例代码:SoftDeleteDemo.rar。主页显示异常请在libman.json上右键恢复前端包。

QQ群

读者交流QQ群:540719365

欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者。

本文地址:https://www.cnblogs.com/coredx/p/18305165.html

论如何直接用EF Core实现创建更新时间、用户审计,自动化乐观并发、软删除和树形查询(上)的更多相关文章

  1. Entity Framework (EF) Core工具创建一对多和多对多的关系

     一. EntirtyFramework(EF)简介 EntirtyFramework框架是一个轻量级的可扩展版本的流行实体框架数据访问技术,微软官方提供的ORM工具让开发人员节省数据库访问的代码时间 ...

  2. EF Core设置字段默认时间

    ---恢复内容开始--- 在EF的官方文档上只提到了用 Fluent API来设置默认值. 但是我们日常开发中,会把公用字段都写成基类.比如行创建时间 在需要默认时间的字段加上一个特性 [Databa ...

  3. .net core EF Core 视图的应用

    由之前的一篇文章<.net core Entity Framework 与 EF Core>我们都已经知道 EF Core 增加了许多特性,并且性能上也有了很大的提升. 但是EF Core ...

  4. 一步步学习EF Core(3.EF Core2.0路线图)

    前言 这几天一直在研究EF Core的官方文档,暂时没有发现什么比较新的和EF6.x差距比较大的东西. 不过我倒是发现了EF Core的路线图更新了,下面我们就来看看 今天我们来看看最新的EF Cor ...

  5. EF Core 使用编译查询提高性能

    今天,我将向您展示这些EF Core中一个很酷的功能,通过使用显式编译的查询,提高查询性能. 不过在介绍具体内容之前,需要说明一点,EF Core已经对表达式的编译使用了缓存:当您的代码需要重用以前执 ...

  6. [翻译 EF Core in Action 1.9] 掀开EF Core的引擎盖看看EF Core内部是如何工作的

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

  7. [翻译 EF Core in Action 1.7] MyFirstEfCoreApp访问的数据库

    Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...

  8. Entity Framework Core(EF Core) 最简单的入门示例

    目录 概述 基于 .NET Core 的 EF Core 入门 创建新项目 更改当前目录 安装 Entity Framework Core 创建模型 创建数据库 使用模型 基于 ASP.NET Cor ...

  9. EF Core 2.1 Raw SQL Queries (转自MSDN)

    Entity Framework Core allows you to drop down to raw SQL queries when working with a relational data ...

  10. EF Core 中多次从数据库查询实体数据,DbContext跟踪实体的情况

    使用EF Core时,如果多次从数据库中查询一个表的同一行数据,DbContext中跟踪(track)的实体到底有几个呢?我们下面就分情况讨论下. 数据库 首先我们的数据库中有一个Person表,其建 ...

随机推荐

  1. 开发人员必知的5种开源协议(GPL、LGPL、BSD、MIT、Apache License)

    软件开源是许多软件企业需要关注的问题,不同的开源软件协议,对应不同的源代码使用限制.只有了解这些开源软件协议,才能更好地使用和回馈开源软件,否则就有可能触犯法律.今天介绍四种常见的开源软件协议: GP ...

  2. Android 13 - Media框架(21)- ACodec(三)

    关注公众号免费阅读全文,进入音视频开发技术分享群! 这一节我们一起来了解 ACodec 是如何通过 configureCodec 方法配置 OMX 组件的,因为 configureCodec 代码比较 ...

  3. Abp vNext 框架 文章

    http://www.vnfan.com/helinbin/tag/Abp%20vNext框架/

  4. 支持表格识别,PaddleOCRSharp最新发布

    PaddleOCRSharp 2.3.0已经发布nuget包. 项目开源地址:https://gitee.com/raoyutian/paddle-ocrsharp 2.3.0更新内容: 1.增加表格 ...

  5. js 留言板(带删除功能)

    本文所用的知识点:创建节点和添加节点 创建节点:document.createElement('li') 添加节点  node(父亲节点).appendChild(child)    child:子节 ...

  6. k8s——daemonset

    daemonset 为每一个匹配的node都部署一个守护进程 # daemonset node:type=logs daemonset 选择节点 - nadeSelector: 只调度到匹配指定的la ...

  7. wireshark常见使用技巧

    (1)wireshark根据报文不同字段排序 举例说明:现在有50000个1588 sync报文,我想根据报文的originalTimestamp(seconds)字段进行排序,该如何操作呢? 操作过 ...

  8. LeetCode 40. Combination Sum II 组合总和 II (C++/Java)

    题目: Given a collection of candidate numbers (candidates) and a target number (target), find all uniq ...

  9. WPS WORD EXCEL 不合并显示

    WPS WORD EXCEL 不合并显示 版本:WPS 12 , 下载时间约是2023 年. 1.在开始菜单里找到 WPS OFFICE - 配置工具 2.点击"高级(A)". 3 ...

  10. m3u8文件转换mp4 ffmpeg

    m3u8文件转换mp4 ffmpeg 命令行执行下面语句: ffmpeg -i input.m3u8 -c copy output.mp4 ffmpeg.exe 和 input.m3u8 放在同一目录 ...