2013-07-11 38 views
1

我試圖使用LINQ to編輯搜索工具創建複合條件,使用匿名過濾方法

我喜歡什麼在where子句(ItemNumber == x和(StatementStatus == SatusA或者StatementStatus過濾== StatusB))

但現在,它是這樣的:

我喜歡什麼在where子句(ItemNumber == X和StatementStatus == SatusA或者StatementStatus == StatusB過濾器)

爲AND具有比OR更高的操作優先權這不是我想要的。 :) 你能幫忙嗎?

using (var ctx = new MyContext()) { 
    Func<Statement, bool> filter = null; 

    if (!string.IsNullOrEmpty(request.ItemNumber)) 
     filter = new Func<Statement, bool>(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber)); 

    if (request.StatusA) 
     filter = filter == null ? new Func<Statement, bool>(s => s.StatementStatus == StatementStatusType.StatusA) : 
      filter.And(s => s.StatementStatus == StatementStatusType.StatusA); 

    if (request.StatusB) 
     filter = filter == null ? new Func<Statement, bool>(s => s.StatementStatus == StatementStatusType.StatusB) : 
      filter.Or(s => s.StatementStatus == StatementStatusType.StatusB); 

    var results = ctx.Statements 
     .Include("StatementDetails") 
     .Include("StatementDetails.Entry") 
     .Where(filter) 
     .Take(100) 
     .Select(s => new StatementSearchResultDTO{ .... 
     } 
} 

+0

你喜歡我的回答?或者你仍然有一些問題? –

回答

1

發生這種情況不是因爲AND具有比OR更高的優先級。實際情況如下:

var firstFilter = ...; // itemNumber 
var secondFilter = ...; // statusA 
var firstAndSecondFilter = firstFilter.And(secondFilter); // itemNumber && statusA 
var thirdFilter = ...; // statusB 
var endFilter = firstAndSecondFilter.Or(thirdFilter) // (itemNumber && statusA) || statusB. 

問題 - 錯誤的控制流。你必須做這樣的事情:

var filterByA = ...; 
var filterByB = ...; 
var filterByAorB = filterByA.Or(filterByB); 
var filterByNumber = ...; 
var endFiler = filterByNumber.And(filterByAorB); 

而且你的代碼是壞的,不只是因爲它的工作原理錯誤的,但因爲它很難在這樣的風格來編寫代碼。理由:

  1. 此代碼不符合DRY principle。你有兩個相同的lambda表達式,檢查StatusA(看看你的三元運算符)和兩個相同的lambda表達式,檢查StatusB
  2. 你有太長的三元運算符與空檢查。這很糟糕,因爲你沒有看到一般圖片,你的眼睛集中在語法問題上。你可以寫和extension method AndNullable for funcs。像這樣:

    static Func<T1, TOut> AndNullable<T1, TOut>(this Func<T1, TOut> firstFunc, Func<T1, TOut> secondFunc) { 
        if (firstFunc != null) { 
         if (secondFunc != null) 
          return firstFunc.And(secondFunc); 
         else 
          return firstFunc; 
        } 
        else { 
         if (secondFunc != null) 
          return secondFunc; 
         else 
          return null; 
        } 
    } 
    

    對於Or也是如此。現在你的代碼可以這樣寫:

    Func<Statement, bool> filter = null; 
    
    if (request.StatusA) 
        filter = s => s.StatementStatus == StatementStatusType.StatusA; 
    
    if (request.StatusB) 
        filter = filter.OrNullable(s => s.StatementStatus == StatementStatusType.StatusB); 
    
    if (!string.IsNullOrEmpty(request.ItemNumber)) 
        filter = filter.AndNullable(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber)); 
    

    閱讀更好。

  3. 您的過濾器是全球過濾器。對於少數過濾條件來說,全局過濾器的寫入更簡單,並且行數很少,但理解過濾器會更復雜。它改寫了這種方式:

    Func<Statement, bool> filterByStatusA = null; 
    Func<Statement, bool> filterByStatusB = null; 
    
    if (request.StatusA) 
        filterByStatusA = s => s.StatementStatus == StatementStatusType.StatusA; 
    
    if (request.StatusB) 
        filterByStatusB = s => s.StatementStatus == StatementStatusType.StatusB; 
    
    Func<Statement, bool> filterByStatuses = filterByStatusA.OrNullable(filterByStatusB); 
    
    Func<Statement, bool> filterByItemNumber = null; 
    if (!string.IsNullOrEmpty(request.ItemNumber)) 
        filterByItemNumber = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber); 
    
    Func<Statement, bool> endFilter = filterByItemNumber.And(filterByStatuses); 
    

好了,我們已經outthinked我們如何能夠通過將它們組合爲Func<..>寫的過濾器,但我們還是有問題。

  1. 如果結果過濾器爲空,我們會得到什麼問題?答案:ArgumentNullException由於documentation。我們必須考慮這個案例。

  2. 使用簡單的Func<...>可以得到什麼問題?那麼,你必須知道IEnumerable<T>IQueryable<T>接口之間的區別。簡單地說,IEnumerable上的所有操作都會導致對所有元素的簡單迭代(呃,它很懶,IEnumerable真的比IQueryable慢)。因此,例如,對具有10000個對該過濾器有害的元素和400個良好的元素的集合的Where(過濾器),Take(100),ToList()組合會導致迭代10100個元素。如果您爲IQueryable編寫了類似的代碼,那麼過濾請求將在數據庫服務器上發送,並且如果您已經在數據庫上配置了索引,則該服務器將僅迭代〜400(或1000,但不是10100)。那麼你的代碼發生了什麼。

    var results = ctx.Statements // you are getting DbSet<Statement> that implements interface IQueryable<Statement> (and IQueryable<T> implements IEnumerable<T>) 
           .Include("StatementDetails") // still IQueryable<Statement> 
           .Include("StatementDetails.Entry") // still IQueryable<Statement> 
           .Where(filter) // Cuz your filter is Func<..> and there are no extension methods on IQueryable that accepts Func<...> as parameter, your IQueryable<Statement> casted automatically to IEnumerable<Statement>. Full collection will be loaded in your memory and only then filtered. That's bad 
           .Take(100) // IEnumerable<Statement> 
    .Select(s => new StatementSearchResultDTO { .... // IEnumerable<Statement> -> IEnumerable<StatementSearchResultDTO> 
    } 
    

好。現在你明白了這個問題。所以,你只需右鍵代碼可以以這種方式所著:

using (var ctx = new MyContext()) { 
    results = ctx.Statements 
     .Include("StatementDetails") 
     .Include("StatementDetails.Entry") 
     .AsQueryable(); 

    if (!string.IsNullOrEmpty(request.ItemNumber)) 
     results = results.Where(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber)); 

    if (request.StatusA) { 
     if (request.StatusB) 
      results = results.Where(s => s.StatementStatus == StatementStatusType.StatusA || 
             s.StatementStatus == StatementStatusType.StatusA); 
     else 
      results = results.Where(s => s.StatementStatus == StatementStatusType.StatusA); 
    } 
    else { 
     if (request.StatusB) { 
      results = results.Where(s => s.StatementStatus == StatementStatusType.StatusB); 
     } 
     else { 
      // do nothing 
     } 
    } 

    results = .Take(100) 
       .Select(s => new StatementSearchResultDTO{ .... 
       }; 

    // .. now you can you results. 
} 

呀,完全醜陋的,但現在你的數據庫解決如何找到滿足篩選報表。因此,這個請求儘可能快。現在我們必須瞭解在我寫的代碼中發生了什麼魔術。讓我們比較一下兩個代碼示例:

results = results.Where(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber)); 

這:

Func<Statement, bool> filter = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber); 
results = results.Where(filter); 

什麼區別?爲什麼第一個更快?回答:當編譯器看到第一個代碼時,它檢查那種類型的resultsIQueryable<T>IEnumerable<T>,這樣括號內的條件可以有Func<Statement, bool>(編譯函數)或Expression<Func<Statement, bool>>(數據,可以在函數中編譯)。編譯器選擇Expression(爲什麼 - 真的不知道,只是選擇)。第一個對象查詢的請求不是在C#語句中編譯,而是在SQL語句中編譯併發送到服務器。由於存在索引,您的SQL服務器可以優化請求。

那麼,更好的方法 - 編寫自己的表達式。有不同的方式來編寫自己的表達式,但有一種方法可以用不難看的語法來編寫它。不能僅從另一個表達式調用一個表達式的問題 - 實體框架不支持並且不能被另一個ORM支持。所以,我們可以使用Pete Montgomery的PredicateBuilder:link。然後在適合我們的表達式上寫兩個簡單的擴展。

public static Expression<Func<T, bool>> OrNullable<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) 
{ 
    if (first != null && second != null) 
     return first.Compose(second, Expression.OrElse); 

    if (first != null) 
     return second; 

    if (second != null) 
} 

而且和And相同。現在我們可以寫我們的過濾器:

{ 
    Expression<Func<Statement, bool>> filterByStatusA = null; 
    Expression<Func<Statement, bool>> filterByStatusB = null; 

    if (request.StatusA) 
     filterByStatusA = s => s.StatementStatus == StatementStatusType.StatusA; 

    if (request.StatusB) 
     filterByStatusB = s => s.StatementStatus == StatementStatusType.StatusB; 

    Expression<Func<Statement, bool>> filterByStatuses = filterByStatusA.OrNullable(filterByStatusB); 

    Expression<Func<Statement, bool>> filterByItemNumber = null; 
    if (!string.IsNullOrEmpty(request.ItemNumber)) 
     filterByItemNumber = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber); 

    Expression<Func<Statement, bool>> endFilter = filterByItemNumber.And(filterByStatuses); 

    requests = ...; 
    if (endFilter != null) 
     requests = requests.Where(endFilter); 
} 

你可以有一個問題,因爲類ExpressionVisitorPredicateBuilder在.NET 4.0 <密封。您可以編寫自己的ExpressionVisitor或從this article複製它。

+0

哦,我忘了說:所有的代碼是未經測試的。有更簡單的例子的測試,但不是數據庫和你的類 –

+0

謝謝你所有的時間回答我的問題。 –

0

OK,這裏就是這樣,我已經解決了它:

filter.And(s => (request.StatusA && s.StatementStatus == StatementStatusType.StatusA) || 
          (request.StatusB && s.StatementStatus == StatementStatusType.StautsB) || 
          !(request.StatusA || request.StatusB)); //None selected = All selected 

有何評論?

+0

好吧,完全錯誤。我現在正在寫你的問題的一個重要答案。從'new MyContext()'行我認爲你正在使用實體框架或其他一些ORM? –

+1

此代碼存在小問題:速度很慢。你可以在每個Statement上調用request.StatusA的getter和request.StatusB的getter,這會導致一些小的性能問題。順便說一句,真正的問題,你使用Func <>,而不是表達式> –