c# - 在 Linq to Entities 和 Linq to Object 之间共享表达式

标签 c# linq-to-entities linq-to-objects func

我试图在 Linq to Entities 调用和其他一些代码之间“共享”一组条件,以减少两个调用之间条件可能不匹配的情况。

我首先声明了我的条件:

private Func<DateTime, Status, bool> _submissionDateExpiredCondition =
(submissionDate, status) => submissionDate < DateTime.Now && status == Status.OK;

private Func<DateTime, Status, bool> _submissionDateWithinOneWeekCondition =
(submissionDate, status) => DateTime.Now < DbFunctions.AddDays(submissionDate, -7) && status == Status.Pending;

private Func<DateTime?, Status, bool> _bidValidityEndPeriodWithinThirtyDaysCondition =
(bidValidityEndPeriod, status) =>  bidValidityEndPeriod.HasValue && DateTime.Now < DbFunctions.AddDays(bidValidityEndPeriod.Value, -30) && (status == Status.OK);

然后,我想在 Linq to Entities where 调用和 if 语句中的 as 函数(或者可能是 Linq to Objects 查询的 where 调用)中的 where 子句中使用这些条件:

myRepository
    .FindAll()
    .Where(x => x.Property == "value" 
    && x.Data.AnotherProperty == true 
    && _submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status) 
    || _submissionDateWithinOneWeekCondition(x.Data.Timestamp, x.Data.Status) 
    || _bidValidityEndPeriodWithinThirtyDaysCondition(x.Data.ValidityStamp, x.Data.Status))

并且(请注意,MyCustomObject 与 myRepository.FindAll() 返回的类型不同)

private void ApplyConditions(List<MyCustomObject> items) {
    foreach(var x in items){
        if(_submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)){
            x.Property = "condition 1";
        }
        else if(_submissionDateWithinOneWeekCondition(x.Data.Timestamp, x.Data.Status))
        {
            x.Property = "condition 2";
        }
        else if(_bidValidityEndPeriodWithinThirtyDaysCondition(x.Data.ValidityStamp, x.Data.Status))
        {
            x.Property = "condition 3";
        }
    }
}

但我不断遇到一些常规问题,例如
LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”。
执行存储库查询时...

我尝试使用谓词构建器构建谓词(按照 https://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/ ),但没有成功。

有人能指出我正确的方向吗?

最佳答案

虽然迟到了,但有人可能会发现我解决问题的方法很有用。 然而,如果不进行一些表达式操作,这是不容易完成的。

主要问题是:在 .Where 的谓词表达式中,您有委托(delegate)的 InitationExpression(即编译后的代码)。 EF 无法找出委托(delegate)中包含哪些逻辑,因此无法将其转换为 SQL。这就是异常的根源。

目标是获得一个 .Where 谓词 lambda 表达式,该表达式在逻辑上与您的表达式等效,但可以被 EF 理解。这意味着我们必须从

Expression<Func<EntityType, bool>> xPredicate = x =>
    x.Property == "value" &&
    x.Data.AnotherProperty == true && 
    _submissionDateExpiredCondition(x.Data.Timestamp, x.Data.Status)
    || ...;

Expression<Func<EntityType, bool>> xPredicate = x =>
    x.Property == "value" &&
    x.Data.AnotherProperty == true && 
    x.Data.Timestamp < DateTime.Now && x.Data.Status == Status.OK
    || ...;

用于

myRepository.FindAll().Where(xPredicate)

,其中 EntityTypeFind 返回的可查询元素类型 - 与 MyCustomObject 不同的类型。

请注意,委托(delegate)的调用将被其定义表达式(lambda 主体)替换,并且 (lambda) 参数 submissionDatestatus 被相应的参数替换调用的表达式。

如果将条件定义为委托(delegate),则它们的内部逻辑会在编译代码中丢失,因此我们必须从 lambda 表达式而不是委托(delegate)开始:

private Expression<Func<DateTime, Status, bool>> _xSubmissionDateExpiredCondition = (submissionDate, status) => submissionDate < DateTime.Now && status == Status.OK;
// getting the delegate as before (to be used in ApplyConditions) is trivial:
private Func<DateTime, Status, bool> _submissionDateExpiredCondition = _xSubmissionDateExpiredCondition.Compile();

// ... other conditions here

使用 lambda 表达式而不是委托(delegate),编译器允许您重写原始谓词,如下所示:

Expression<Func<EntityType, bool>> xPredicate = x =>
    x.Property == "value" &&
    x.Data.AnotherProperty == true && 
    _xSubmissionDateExpiredCondition.Compile()(x.Data.Timestamp, x.Data.Status)
    || ...;

,当然 EF 不会比以前更好地理解。然而,我们实现的是条件的内部逻辑是表达式树的一部分。所以所缺少的只是一些魔法:

xPredicate = MAGIC(xPredicate);

MAGIC 的作用:查找委托(delegate)的 InitationExpression,该委托(delegate)是对 lambda 表达式进行 Compile() 方法调用的结果,并且将其替换为 lambda 的主体,但请确保将主体中的 lambda 参数替换为调用的参数表达式。

这是我的实现。实际上,MAGIC 在这里被称为 Express.Prepare,这稍微不那么具体。

/// <summary>
/// Helps in building expressions.
/// </summary>
public static class Express
{

    #region Prepare

    /// <summary>
    /// Prepares an expression to be used in queryables.
    /// </summary>
    /// <returns>The modified expression.</returns>
    /// <remarks>
    /// The method replaces occurrences of <see cref="LambdaExpression"/>.Compile().Invoke(...) with the body of the lambda, with it's parameters replaced by the arguments of the invocation.
    /// Values are resolved by evaluating properties and fields only.
    /// </remarks>
    public static Expression<TDelegate> Prepare<TDelegate>(this Expression<TDelegate> lambda) => (Expression<TDelegate>)new PrepareVisitor().Visit(lambda);

    /// <summary>
    /// Wrapper for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
    /// </summary>
    public static Expression<Func<T1, TResult>> Prepare<T1, TResult>(Expression<Func<T1, TResult>> lambda) => lambda.Prepare();

    /// <summary>
    /// Wrapper for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
    /// </summary>
    public static Expression<Func<T1, T2, TResult>> Prepare<T1, T2, TResult>(Expression<Func<T1, T2, TResult>> lambda) => lambda.Prepare();

    // NOTE: more overloads of Prepare here.

    #endregion

    /// <summary>
    /// Evaluate an expression to a simple value.
    /// </summary>
    private static object GetValue(Expression x)
    {
        switch (x.NodeType)
        {
            case ExpressionType.Constant:
                return ((ConstantExpression)x).Value;
            case ExpressionType.MemberAccess:
                var xMember = (MemberExpression)x;
                var instance = xMember.Expression == null ? null : GetValue(xMember.Expression);
                switch (xMember.Member.MemberType)
                {
                    case MemberTypes.Field:
                        return ((FieldInfo)xMember.Member).GetValue(instance);
                    case MemberTypes.Property:
                        return ((PropertyInfo)xMember.Member).GetValue(instance);
                    default:
                        throw new Exception(xMember.Member.MemberType + "???");
                }
            default:
                // NOTE: it would be easy to compile and invoke the expression, but it's intentionally not done. Callers can always pre-evaluate and pass a captured member.
                throw new NotSupportedException("Only constant, field or property supported.");
        }
    }

    /// <summary>
    /// <see cref="ExpressionVisitor"/> for <see cref="Prepare{TDelegate}(Expression{TDelegate})"/>.
    /// </summary>
    private sealed class PrepareVisitor : ExpressionVisitor
    {
        /// <summary>
        /// Replace lambda.Compile().Invoke(...) with lambda's body, where the parameters are replaced with the invocation's arguments.
        /// </summary>
        protected override Expression VisitInvocation(InvocationExpression node)
        {
            // is it what we are looking for?
            var call = node.Expression as MethodCallExpression;
            if (call == null || call.Method.Name != "Compile" || call.Arguments.Count != 0 || call.Object == null || !typeof(LambdaExpression).IsAssignableFrom(call.Object.Type))
                return base.VisitInvocation(node);

            // get the lambda
            var lambda = call.Object as LambdaExpression ?? (LambdaExpression)GetValue(call.Object);

            // get the expressions for the lambda's parameters
            var replacements = lambda.Parameters.Zip(node.Arguments, (p, x) => new KeyValuePair<ParameterExpression, Expression>(p, x));

            // return the body with the parameters replaced
            return Visit(new ParameterReplaceVisitor(replacements).Visit(lambda.Body));
        }
    }

    /// <summary>
    /// <see cref="ExpressionVisitor"/> to replace parameters with actual expressions.
    /// </summary>
    private sealed class ParameterReplaceVisitor : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, Expression> _replacements;

        /// <summary>
        /// Init.
        /// </summary>
        /// <param name="replacements">Parameters and their respective replacements.</param>
        public ParameterReplaceVisitor(IEnumerable<KeyValuePair<ParameterExpression, Expression>> replacements)
        {
            _replacements = replacements.ToDictionary(kv => kv.Key, kv => kv.Value);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            Expression replacement;
            return _replacements.TryGetValue(node, out replacement) ? replacement : node;
        }
    }
}

关于c# - 在 Linq to Entities 和 Linq to Object 之间共享表达式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35783864/

相关文章:

C# => JAVA : Filling a static ArrayList on declaration. 可能吗?

c# - 仅当来自比较可枚举的所有(非原始)值都包含在目标可枚举中时才返回结果的查询

entity-framework - Entity Framework 不查询派生类 - DbOfTypeExpression 中的错误

c# - 使用linq to实体进行嵌套查询

c# - 使用 Linq 的隐式转换

c# - LINQ 表达式中的异常处理

c# - 使用服务时如何避免冲突? (.NET C#)

c# - 支持重复的多维键的字典?

c# - ASP.NET MVC3 : ValidationMessageFor on new line

c# - Linq 确保没有两个对象具有相同的属性值