在规范模式中组合规范时,LINQ 表达式无法转换为 Sql

LINQ expressions can not be translated to Sql when combining specifications in Specification Pattern

我正在使用规范模式执行数据库过滤,并避免在内存中执行此操作 (I roughly followed this article)。我的基本规范 class 是这样的:

    public abstract class Specification<T> : ISpecification<T>{

    public abstract Expression<Func<T, bool>> FilterExpr();

    public bool IsSatisfied(T entity)
    {
        Func<T, bool> func = this.FilterExpr().Compile();
        return func(entity);
    }

    public Specification<T> And(Specification<T> otherSpec)
    {
        return new CombinedSpecification<T>(this, otherSpec);
    }
}

从这个基本规范 class 中派生出多个强类型规范,它们各自运行良好。但是,当尝试组合此类规范并评估我的存储库中的 CombinedSpecification 时,问题就出现了 class:

System.InvalidOperationException: The LINQ expression 'DbSet() .Where(c => c.ClientCode == __client_0 && c.Status == "Efective")' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

这里是 CombinedSpecification class:

    internal class CombinedSpecification<T> : Specification<T>
    {
        private Specification<T> leftSpec;
        private Specification<T> rightSpec;

        public CombinedSpecification(Specification<T> aSpec, Specification<T> otherSpec)
        {
            this.leftSpec = aSpec;
            this.rightSpec = otherSpec;
        }

        public override Expression<Func<T, bool>> FilterExpr()
        {
            Expression<Func<T, bool>> firstExpr = this.leftSpec.FilterExpr();
            Expression<Func<T, bool>> secondExpr = this.rightSpec.FilterExpr();

            BinaryExpression combined = Expression.AndAlso(firstExpr.Body, secondExpr.Body);

            return Expression.Lambda<Func<T, bool>>(combined, firstExpr.Parameters.Single());
        }
    }

需要说明的是,按单一规范过滤效果很好,但 LinQ 表达式在组合它们时似乎无法翻译(这对我来说似乎很奇怪,因为我没有使用 [= 不支持的任何方法31=]据我所知)。我避免显示存储库 class 以减少这个问题的数量,但无论如何这里是我的 Find() 方法的相关行:

    public IEnumerable<TEntity> Find(Specification<TEntity> spec)
        {
            IQueryable<TEntity> result = this.dbSet;

            if (spec != null)
            {
                result = result.Where(spec.FilterExpr());
            }

            return result.ToList();
        }

在此先感谢您的帮助,我希望我的第一个问题已经说清楚了!

问题在于,在您的 lambda 表达式组合实现中,您使用了一个组合了两个 lambda 表达式主体的主体,但仅包含第一个 lambda 表达式的参数 (firstExpr.Parameters.Single())。

这样第二个主体仍然引用一个不同的参数(即使它在视觉上看起来可能相同,lambda表达式中的参数由reference,而不是像你在编译时创建它们时所想的那样按名称命名。

换句话说,就是您在错误消息中看到的内容

c => c.ClientCode == __client_0 && c.Status == "Efective"

其实是这样的

p0 => p0.ClientCode == __client_0 && p1.Status == "Efective"

这当然是无效的,无法翻译(或编译 - 尝试调用 Compile(),你会得到运行时异常)。

您需要确保两个操作数以及生成的 lambda 表达式使用同一个参数实例。

您仍然可以重复使用其中一个操作数的参数,但您必须将另一个操作数的主体重新绑定到同一参数。这通常是通过小型自定义 ExpressionVisitor 实现的,它会在表达式树中找到所有出现的参数,并将它们替换为另一个参数(或表达式)——非常类似于 string.Replace,但对于表达式:


public static partial class ExpressionExtensions
{
    public static Expression ReplaceParameter(this Expression target, ParameterExpression parameter, Expression value)
        => new ParameterReplacer { Parameter = parameter, Value = value }.Visit(target);

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Parameter;
        public Expression Value;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Parameter ? Value : node;
    }
}

并用类似这样的东西修改组合实现


public override Expression<Func<T, bool>> FilterExpr()
{
    var firstExpr = this.leftSpec.FilterExpr();
    var secondExpr = this.rightSpec.FilterExpr();

    var parameter = firstExpr.Parameters[0]; // <--
    var combined = Expression.AndAlso(
        firstExpr.Body, 
        secondExpr.Body.ReplaceParameter(secondExpr.Parameters[0], parameter) // <--
    );

    return Expression.Lambda<Func<T, bool>>(combined, parameter); 
}