2016-03-03 74 views
1

我試圖在Linq到Entities調用和一些其他代碼之間「共享」一組條件,以減少兩個調用之間條件的可能不匹配。在Linq到Entities和Linq到對象之間共享表達式

我開始通過聲明我的條件:

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

然後我想用我的where子句中既有LINQ到實體,其中呼叫和功能於一身的if語句(或可能是其中內這些條件一個LINQ的調用對象查詢):

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

,但我一直撞到常規問題,如
執行庫查詢時 The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.
...

我已經試過建設有一個謂語製造商(按https://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/),但沒有運氣的謂詞。

任何人都可以指向正確的方向嗎?

+0

看一看這個問題,可能你指出正確的方向:http://stackoverflow.com/questions/5284912/the-linq-expression-node-type-in​​voke-is-not-支持在LINQ到實體 – JanR

+0

@JanR - 我見過這個例子。這裏大多數例子的問題是它們都與Linq to Entities相關,並且它們都具有實體類型作爲表達式/ func的輸入參數。 我不得不縮小到特定的輸入類型(例如DateTime和Status),因爲我想使用我的實體類型和自定義對象類型上的funcs。 –

+0

是否有可能使您的實體框架實體類型和您的MyCustomObject類型實現相同的接口?這可以讓你將你的過濾器作爲表達式寫入該接口。 –

回答

1

晚會晚了,但有人可能會發現我的攻擊方式有用。但是,如果不進行某些表達式操作就不容易做到。

主要問題是:在.Where的謂詞表達式中,您有代表(即編譯代碼)的InvocationExpression。 EF沒有辦法找出該委託中包含的邏輯,因此無法將其轉換爲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

請注意,委託的調用將由它的定義表達式(lambda body)替換,其中(lambda)參數submissionDatestatus被調用的各個參數表達式替換。

如果您定義的條件爲代表,其內在邏輯是失去了在編譯的代碼,所以我們必須要與lambda表達式,而不是代表開始:

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表達式,而不是委託,編譯器可以讓你像這樣重寫原始謂詞:

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也:找一個委託,它是一個lambda表達式Compile()方法調用的結果的InvocationExpression,並與拉姆達的身體代替它,但一定要將主體中的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; 
     } 
    } 
}