如何在通用存储库中使用 EF Core 8 的 ExecuteUpdate() 和 ExecuteDelete(),同时保持其接口与 EF Core 无关

问题描述 投票:0回答:1

我正在使用 .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 无关?

c# entity-framework-core repository-pattern ef-core-8.0
1个回答
0
投票

这是替换表达式中类型的解决方案。我希望它是通用解决方案,不仅可以用于替换

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
是多年经验的结果,对其他人的支持可能具有挑战性。

© www.soinside.com 2019 - 2024. All rights reserved.