How to use EF Core 8's ExecuteUpdate() and ExecuteDelete() in a generic repository while keeping its interface agnostig from EF Core

245 views Asked by At

I'm developing a web application using .NET 8 and Clean Architecture.
There I'm trying to implement a generic repository that includes two methods for the bulk-update and bulk-delete
that use EF Core 8's ExecuteUpdate() and ExecuteDelete() methods, so that I can update/delete the database without being forced to select the entries that must be deleted in advance.

MyRepository.cs (located in the Infrastructure Layer)

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

The code sample above needs to include the Microsoft.EntityFrameworkCore.Query namespace to use the SetPropertyCalls class in the BulkUpdate()method.

In my understanding, the according interface IMyRepository must be located in the Application Layer
since it's going to be used from most of my Application Services.

IMyRepository.cs (located in the Application Layer)

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

To achieve this, I would have to include the Microsoft.EntityFrameworkCore.Query namespace also in the interface.
But in my understanding, the Application Layer should be technology-agnostic, i.e. it's not allowed to include Microsoft.EntityFrameworkCore there.

How can I obtain a generic repository that uses EF Core 8's ExecuteUpdate() and ExecuteDelete()methods while keeping the application layer (and the interface) agnostic from EF Core?

1

There are 1 answers

1
Svyatoslav Danyliv On

This is solution for replacing types in expression. I hope it is universal solution and can be used not just for replacing types for SetPropertyCalls

Usage in your repository:

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

Introduce interfaces for mimic EF Core classes:

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

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

And core of this solution 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);
        }
    }
}

As I said in comments, generic repository pattern is anti-pattern for EF Core. Do not introduce thing that will slowdown your development and increase project complexity. ExpressionTypeMapper is a result of many years of experience and can be challenging for others to support.