我正在使用 .NET 8 和 Clean Architecture 开发一个 Web 应用程序。
我正在尝试实现一个通用存储库,其中包括批量更新和批量删除的两种方法
使用 EF Core 8 的
ExecuteUpdate()
和 ExecuteDelete()
方法,这样我就可以更新/删除数据库,而无需被迫选择必须提前删除的条目。
MyRepository.cs(位于基础设施层)
public class MyRepository<TEntity> : IMyRepository<TEntity> where TEntity : class, IBaseEntity, new()
{
protected readonly IMyDataContext DataContext;
protected MyRepository(IMyDataContext dataContext)
{
DataContext = dataContext;
}
// src: https://stackoverflow.com/a/75799111/5757823
public int BulkUpdate(Expression<Func<TEntity, bool>> query, Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> expression)
{
return context.Set<TEntity>().Where(query).ExecuteUpdate(expression);
}
public int BulkDelete(Expression<Func<TEntity, bool>> query)
{
return this.context.Set<TEntity>().Where(query).ExecuteDelete();
}
}
上面的代码示例需要包含
Microsoft.EntityFrameworkCore.Query
命名空间才能在 SetPropertyCalls
方法中使用 BulkUpdate()
类。
以我的理解,相应的接口
IMyRepository
必须位于应用层IMyRepository.cs(位于应用层)
public interface IMyRepository<TEntity> where TEntity : class, IBaseEntity, new()
{
int BulkUpdate(Expression<Func<TEntity, bool>> query, Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> expression);
int BulkDelete(Expression<Func<TEntity, bool>> query);
}
要实现此目的,我还必须在界面中包含
Microsoft.EntityFrameworkCore.Query
命名空间。Microsoft.EntityFrameworkCore
。
如何获取使用 EF Core 8 的
ExecuteUpdate()
和 ExecuteDelete()
方法的通用存储库,同时保持应用程序层(和接口)与 EF Core 无关?
这是替换表达式中类型的解决方案。我希望它是通用解决方案,不仅可以用于替换
SetPropertyCalls
的类型
在您的存储库中的使用:
public int BulkUpdate(Expression<Func<TEntity, bool>> query, Expression<Func<ISetPropertyCalls<TEntity>, ISetPropertyCalls<TEntity>>> expression)
{
var transformed = RepositoryUtils.TransformUpdateExpression<TEntity(expression);
return context.Set<TEntity>().Where(query).ExecuteUpdate(transformed);
}
引入模拟 EF Core 类的接口:
public interface ISetPropertyCalls<TSource>
{
ISetPropertyCalls<TSource> SetProperty<TProperty>(
Func<TSource, TProperty> propertyExpression,
Func<TSource, TProperty> valueExpression);
ISetPropertyCalls<TSource> SetProperty<TProperty>(
Func<TSource, TProperty> propertyExpression,
TProperty valueExpression);
}
介绍
RepositoryUtils
:
public static class RepositoryUtils
{
public static Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> TransformUpdateExpression<TSource>(
Expression<Func<ISetPropertyCalls<TSource>, ISetPropertyCalls<TSource>>> setPropertyCalls)
{
var newExpression = ExpressionTypeMapper.ReplaceTypes(setPropertyCalls,
new Dictionary<Type, Type>()
{ { typeof(ISetPropertyCalls<TSource>), typeof(SetPropertyCalls<TSource>) } });
return (Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>>)newExpression;
}
}
这个解决方案的核心
ExpressionTypeMapper
:
public static class ExpressionTypeMapper
{
public static Expression ReplaceTypes(Expression expression, IDictionary<Type, Type> replacements)
{
var newExpression = new TypeReplacementVisitor(replacements).Visit(expression);
return newExpression;
}
class TypeReplacementVisitor : ExpressionVisitor
{
private readonly IDictionary<Type, Type?> _typeReplacementCache = new Dictionary<Type, Type?>();
private readonly Stack<Dictionary<ParameterExpression, ParameterExpression>> _currentParameters = new();
public TypeReplacementVisitor(IDictionary<Type, Type> typeReplacement)
{
foreach (var r in typeReplacement)
{
_typeReplacementCache.Add(r.Key, r.Value);
}
}
private bool TryMapType(Type type, [NotNullWhen(true)] out Type? replacement)
{
if (_typeReplacementCache.TryGetValue(type, out replacement))
return replacement != null;
if (type.IsGenericType)
{
if (type.GetGenericArguments().Any(NeedsMapping))
{
var types = type.GetGenericArguments().Select(MapType).ToArray();
replacement = type.GetGenericTypeDefinition().MakeGenericType(types);
}
}
_typeReplacementCache.Add(type, replacement);
return replacement != null;
}
MemberInfo ReplaceMember(MemberInfo memberInfo, Type targetType)
{
var newMembers = targetType.GetMember(memberInfo.Name);
if (newMembers.Length == 0)
throw new InvalidOperationException($"There is no member '{memberInfo.Name}' in type '{targetType.FullName}'");
if (newMembers.Length > 1)
throw new InvalidOperationException($"Ambiguous member '{memberInfo.Name}' in type '{targetType.FullName}'");
return newMembers[0];
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
var replacements = node.Parameters.ToDictionary(p => p,
p => TryMapType(p.Type, out var replacementType)
? Expression.Parameter(replacementType, p.Name)
: p);
_currentParameters.Push(replacements);
var newBody = Visit(node.Body);
_currentParameters.Pop();
if (ReferenceEquals(newBody, node.Body) && replacements.All(pair => pair.Key != pair.Value))
{
// nothing changed
return node;
}
return Expression.Lambda(newBody, replacements.Select(pair => pair.Value));
}
protected override Expression VisitParameter(ParameterExpression node)
{
foreach (var dict in _currentParameters)
{
if (dict.TryGetValue(node, out var newNode))
return newNode;
}
return base.VisitParameter(node);
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression != null && TryMapType(node.Expression.Type, out var replacement))
{
var expr = Visit(node.Expression);
if (expr.Type != replacement)
throw new InvalidOperationException($"Invalid replacement of '{node.Expression}' to type '{replacement.FullName}'.");
var prop = replacement.GetProperty(node.Member.Name);
if (prop == null)
throw new InvalidOperationException($"Property not found in target type: {replacement.FullName}.{node.Member.Name}");
return Expression.MakeMemberAccess(expr, prop);
}
return base.VisitMember(node);
}
protected override Expression VisitNew(NewExpression node)
{
if (TryMapType(node.Type, out var replacement) && node.Constructor != null)
{
var paramTypes = node.Constructor.GetParameters()
.Select(p => p.ParameterType)
.ToArray();
var ctor = replacement.GetConstructor(paramTypes);
if (ctor == null)
{
var name = replacement.FullName + "." + node.Constructor.Name + "(" +
string.Join(", ", paramTypes.Select(t => t.Name)) + ")";
throw new InvalidOperationException($"Constructor not found in target type: {name}");
}
var newArguments = node.Arguments.Select(Visit);
if (node.Members != null)
{
var newMembers = node.Members.Select(m => ReplaceMember(m, replacement));
var newExpression = Expression.New(ctor, newArguments!, newMembers);
return newExpression;
}
else
{
var newExpression = Expression.New(ctor, newArguments!);
return newExpression;
}
}
return base.VisitNew(node);
}
protected override Expression VisitMemberInit(MemberInitExpression node)
{
if (TryMapType(node.Type, out var replacement))
{
var newExpression = (NewExpression)Visit(node.NewExpression);
var newBindings = node.Bindings.Select(b =>
{
switch (b.BindingType)
{
case MemberBindingType.Assignment:
{
var mab = (MemberAssignment)b;
return Expression.Bind(ReplaceMember(mab.Member, replacement),
Visit(mab.Expression));
}
case MemberBindingType.MemberBinding:
{
throw new NotImplementedException();
}
case MemberBindingType.ListBinding:
{
throw new NotImplementedException();
}
default:
throw new ArgumentOutOfRangeException();
}
});
var newMemberInit = Expression.MemberInit(newExpression, newBindings);
return newMemberInit;
}
return base.VisitMemberInit(node);
}
private Type MapType(Type type)
{
return TryMapType(type, out var newType) ? newType : type;
}
private bool NeedsMapping(Type type)
{
return TryMapType(type, out _);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.DeclaringType != null)
{
var newDeclaringType = MapType(node.Method.DeclaringType);
if (newDeclaringType != node.Method.DeclaringType ||
node.Object != null && NeedsMapping(node.Object.Type) ||
node.Method.GetParameters().Any(p => NeedsMapping(p.ParameterType)) ||
node.Method.IsGenericMethod && node.Method.GetGenericArguments().Any(NeedsMapping))
{
var newObject = Visit(node.Object);
var newArguments = node.Arguments.Select(Visit).ToArray();
var newGenericArguments = Type.EmptyTypes;
if (node.Method.IsGenericMethod)
newGenericArguments = node.Method.GetGenericArguments().Select(MapType).ToArray();
if (newObject != null)
{
var newCall = Expression.Call(newObject!, node.Method.Name, newGenericArguments, newArguments!);
return newCall;
}
else
{
var newCall = Expression.Call(newDeclaringType, node.Method.Name, newGenericArguments, newArguments!);
return newCall;
}
}
}
return base.VisitMethodCall(node);
}
}
}
正如我在评论中所说,通用存储库模式是 EF Core 的反模式。不要引入会减慢您的开发速度并增加项目复杂性的东西。
ExpressionTypeMapper
是多年经验的结果,对其他人的支持可能具有挑战性。