在Linq到实体和Linq到对象之间共享表达式 [英] Share expressions between Linq to Entities and Linq to Objects

查看:79
本文介绍了在Linq到实体和Linq到对象之间共享表达式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在Linq to Entities调用与其他代码之间共享"一组条件,以减少两个调用之间的条件不匹配.

I'm trying to "share" a set of conditions between a Linq to Entities call and a some other code, to reduce possible mismatches in conditions between the two calls.

我首先声明了自己的状况:

I started off by declaring my conditions:

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语句(或可能是Linq to Objects查询的where调用)中的函数中的where子句中使用这些条件:

I then want to use these conditions inside my where clause in both a Linq to Entities where call and as functions in an if statement (or possibly the where call of a Linq to Objects query):

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()返回的类型不同)

and (please note that MyCustomObject is not the same type as returned by 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";
        }
    }
}

但是我一直碰到像
这样的常规问题 The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.
在执行存储库查询时...

But I keep bumping into the regular issues like
The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.
when executing the repository query...

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

I've tried building a predicate with a predicate builder (as per https://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/) but no luck.

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

Can anyone point me in the right direction?

推荐答案

晚些时候参加聚会,但是有人可能会发现我解决问题的方法很有用. 但是,如果没有一些表达式操作,就很难做到.

Late to the party, but someone may find my way of attacking the problem useful. However it can't easily be done without some expression manipulation.

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

Main problem is: inside the .Where's predicate expression you have InvocationExpressions of delegates (i.e. compiled code). EF has no way to find out what logic is baked into that delegates and thus won't be able to translate it into SQL. That's where the exception originates.

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

The goal is to get a .Where predicate lambda expression that is logically equivalent to yours, yet understandable by EF. That means we have to get from

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不同.

, where EntityTypeis the element type of the queryable returned by Find - the one that is different from MyCustomObject.

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

Note that the invocation of the delegate is being replaced by it's defining expression (lambda body), with the (lambda) parameters submissionDate and status replaced by the respective argument expressions of the invocation.

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

If you define the conditions as delegates, their internal logic is lost in compiled code, so we have to start off with lambda expressions rather than delegates:

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表达式而不是委托,编译器可让您重写原始谓词,如下所示:

Using the lambda expression rather than the delegate, the compiler lets you rewrite the original predicate like this:

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

,当然EF不会比以前更了解.但是,我们实现的是条件的内部逻辑是表达式树的一部分.因此,所缺少的只是某种魔术:

, which of course EF won't understand any better than before. What we however achieved is that the condition's internal logic is part of the expression tree. So all that's missing is some magic:

xPredicate = MAGIC(xPredicate);

MAGIC的作用:在lambda表达式上找到作为Compile()方法调用结果的委托的InvocationExpression,并将其替换为lambda的主体,但请确保将其替换为lambda参数.正文以及调用的参数表达式.

What MAGIC does: Find an InvocationExpression of a delegate that is the result of a Compile() method call on a lambda expression and replace it with the lambda's body, but make sure to replace the lambda parameters in the body with the argument expressions of the invocation.

这里是我的实现.实际上,在这里MAGIC被称为Express.Prepare,它的不确定性要小一些.

And here my implementation. Actually, MAGIC is called Express.Prepare here, which is slightly less unspecific.

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

这篇关于在Linq到实体和Linq到对象之间共享表达式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆