2016-02-11 56 views
1

花了很長時間解決了這個問題,我想分享解決方案。使用運行時指定返回類型生成多參數LINQ搜索查詢

背景

我維護與管理訂單的主要功能的大型Web應用程序。它是一個使用EF6進行數據的C#應用​​程序MVC。

有很多搜索屏幕。搜索屏幕都有多個參數並返回不同的對象類型。

的問題

每個搜索屏幕有:

  • 一個ViewModel與搜索參數
  • A控制器的方法來處理搜索事件
  • 的方法來拉正確的數據對於該屏幕
  • 將所有搜索過濾器應用於數據集的方法
  • 的成果轉化爲新的結果的方法視圖模型
  • 結果視圖模型

這迅速增加。我們有大約14個不同的搜索屏幕,這意味着大約84個模型&處理這些搜索的方法。

我的目標

我希望能夠創建一個類,類似於目前的搜索參數視圖模型,會從基SEARCHQUERY類繼承,使得我的控制器可以簡單地觸發運行到搜索填充同一對象的結果字段。

我的理想狀態的一個例子(因爲它是一個熊解釋)

採取以下類結構:

public class Order 
{ 
    public int TxNumber; 
    public Customer OrderCustomer; 
    public DateTime TxDate; 
} 

public class Customer 
{ 
    public string Name; 
    public Address CustomerAddress; 
} 

public class Address 
{ 
    public int StreetNumber; 
    public string StreetName; 
    public int ZipCode; 
} 

假設我有很多的那些記錄可查詢的格式 - - 一個EF DBContext對象,一個XML對象,不管 - 我想搜索它們。首先,我創建了一個特定於我的ResultType的派生類(在本例中爲Order)。

public class OrderSearchFilter : SearchQuery 
{ 
    //this type specifies that I want my query result to be List<Order> 
    public OrderSearchFilter() : base(typeof(Order)) { } 

    [LinkedField("TxDate")] 
    [Comparison(ExpressionType.GreaterThanOrEqual)] 
    public DateTime? TransactionDateFrom { get; set; } 

    [LinkedField("TxDate")] 
    [Comparison(ExpressionType.LessThanOrEqual)] 
    public DateTime? TransactionDateTo { get; set; } 

    [LinkedField("")] 
    [Comparison(ExpressionType.Equal)] 
    public int? TxNumber { get; set; } 

    [LinkedField("Order.OrderCustomer.Name")] 
    [Comparison(ExpressionType.Equal)] 
    public string CustomerName { get; set; } 

    [LinkedField("Order.OrderCustomer.CustomerAddress.ZipCode")] 
    [Comparison(ExpressionType.Equal)] 
    public int? CustomerZip { get; set; } 
} 

我使用屬性來指定目標與resultType任何給定的搜索領域被鏈接到什麼字段/屬性,以及比較類型(== <> < => =!=)。空白LinkedField意味着搜索字段的名稱與目標對象字段的名稱相同。

採用這種配置,僅我應該需要對於給定的搜索的事情是:

  • 像一個以上
  • 數據源

沒有其他scenario-甲填充搜索對象應該要求具體的編碼!

回答

2

解決方案

對於初學者來說,我們創造:

public abstract class SearchQuery 
{ 
    public Type ResultType { get; set; } 
    public SearchQuery(Type searchResultType) 
    { 
     ResultType = searchResultType; 
    } 
} 

我們也將創造我們上面用來定義搜索欄的屬性:

protected class Comparison : Attribute 
    { 
     public ExpressionType Type; 
     public Comparison(ExpressionType type) 
     { 
      Type = type; 
     } 
    } 

    protected class LinkedField : Attribute 
    { 
     public string TargetField; 
     public LinkedField(string target) 
     { 
      TargetField = target; 
     } 
    } 

對於每個搜索字段,我們不僅需要知道進行了什麼搜索,還需要知道搜索是否完成。例如,如果「TxNumber」的值爲空,我們不想運行該搜索。因此,我們創建一個SearchField對象,除了實際的搜索值外,還包含兩個表達式:一個表示執行搜索,另一個驗證是否應該應用搜索。

private class SearchFilter<T> 
    { 
     public Expression<Func<object, bool>> ApplySearchCondition { get; set; } 
     public Expression<Func<T, bool>> SearchExpression { get; set; } 
     public object SearchValue { get; set; } 

     public IQueryable<T> Apply(IQueryable<T> query) 
     { 
      //if the search value meets the criteria (e.g. is not null), apply it; otherwise, just return the original query. 
      bool valid = ApplySearchCondition.Compile().Invoke(SearchValue); 
      return valid ? query.Where(SearchExpression) : query; 
     } 
    } 

一旦我們已經創建了所有過濾器,我們需要做的是循環通過他們,並呼籲「應用」的方法對我們的數據!簡單!

下一步是創建驗證表達式。我們將根據Type來做到這一點;每個int?是否與每個其他int一樣被驗證?

private static Expression<Func<object, bool>> GetValidationExpression(Type type) 
    { 
     //throw exception for non-nullable types (strings are nullable, but is a reference type and thus has to be called out separately) 
     if (type != typeof(string) && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))) 
      throw new Exception("Non-nullable types not supported."); 

     //strings can't be blank, numbers can't be 0, and dates can't be minvalue 
     if (type == typeof(string )) return t => !string.IsNullOrWhiteSpace((string)t); 
     if (type == typeof(int? )) return t => t != null && (int)t >= 0; 
     if (type == typeof(decimal?)) return t => t != null && (decimal)t >= decimal.Zero; 
     if (type == typeof(DateTime?)) return t => t != null && (DateTime?)t != DateTime.MinValue; 

     //everything else just can't be null 
     return t => t != null; 
    } 

這就是我爲我的應用程序所需要的,但肯定會有更多的驗證可以完成。

搜索表達式稍微複雜一些,並且需要一個解析器來「取消限定」字段/屬性名稱(可能有更好的單詞,但如果是這樣,我不知道它)。基本上,如果我將「Order.Customer.Name」指定爲鏈接字段,並且正在通過Orders進行搜索,則需要將其轉換爲「Customer.Name」,因爲Order對象內沒有Order Field。或者至少我不希望。 :)這是不確定的,但我認爲接受和糾正完全限定的對象名稱比支持該邊緣案例更好。

public static List<string> DeQualifyFieldName(string targetField, Type targetType) 
    { 
     var r = targetField.Split('.').ToList(); 
     foreach (var p in targetType.Name.Split('.')) 
      if (r.First() == p) r.RemoveAt(0); 
     return r; 
    } 

這只是直接的文本解析,並返回字段名稱在「levels」(例如「Customer」|「Name」)中。

好的,讓我們一起找到我們的搜索表達。

private Expression<Func<T, bool>> GetSearchExpression<T>(
     string targetField, ExpressionType comparison, object value) 
    { 
     //get the property or field of the target object (ResultType) 
     //which will contain the value to be checked 
     var param = Expression.Parameter(ResultType, "t"); 
     Expression left = null; 
     foreach (var part in DeQualifyFieldName(targetField, ResultType)) 
      left = Expression.PropertyOrField(left == null ? param : left, part); 

     //Get the value against which the property/field will be compared 
     var right = Expression.Constant(value); 

     //join the expressions with the specified operator 
     var binaryExpression = Expression.MakeBinary(comparison, left, right); 
     return Expression.Lambda<Func<T, bool>>(binaryExpression, param); 
    } 

還不錯!我們試圖創建的是,例如:

t => t.Customer.Name == "Searched Name" 

其中,t是我們的ReturnType - 一個Order,在這種情況下。首先我們創建參數t。然後,我們遍歷屬性/字段名稱的各個部分,直到我們擁有完整的目標對象標題(將它命名爲「左」,因爲它是我們比較的左側)。我們比較的「正確」一面很簡單:用戶提供的常數。

然後我們創建二進制表達式並將其轉化爲lambda。容易脫落日誌!無論如何,如果從日誌中墮落需要無數小時的挫折和失敗的方法。但我離題了。

現在我們已經有了所有的東西;我們需要的是組裝我們的查詢方法:

protected IQueryable<T> ApplyFilters<T>(IQueryable<T> data) 
    { 
     if (data == null) return null; 
     IQueryable<T> retVal = data.AsQueryable(); 

     //get all the fields and properties that have search attributes specified 
     var fields = GetType().GetFields().Cast<MemberInfo>() 
           .Concat(GetType().GetProperties()) 
           .Where(f => f.GetCustomAttribute(typeof(LinkedField)) != null) 
           .Where(f => f.GetCustomAttribute(typeof(Comparison)) != null); 

     //loop through them and generate expressions for validation and searching 
     try 
     { 
      foreach (var f in fields) 
      { 
       var value = f.MemberType == MemberTypes.Property ? ((PropertyInfo)f).GetValue(this) : ((FieldInfo)f).GetValue(this); 
       if (value == null) continue; 
       Type t = f.MemberType == MemberTypes.Property ? ((PropertyInfo)f).PropertyType : ((FieldInfo)f).FieldType; 
       retVal = new SearchFilter<T> 
       { 
        SearchValue = value, 
        ApplySearchCondition = GetValidationExpression(t), 
        SearchExpression = GetSearchExpression<T>(GetTargetField(f), ((Comparison)f.GetCustomAttribute(typeof(Comparison))).Type, value) 
       }.Apply(retVal); //once the expressions are generated, go ahead and (try to) apply it 
      } 
     } 
     catch (Exception ex) { throw (ErrorInfo = ex); } 
     return retVal; 
    } 

基本上,我們只要抓住字段列表/在派生類(即鏈接)的屬性,從他們創造一個SearchFilter對象,並應用它們。

清理

還有更多一點,當然。例如,我們正在用字符串指定對象鏈接。如果有錯字呢?

在我的情況,我有類檢查時,它旋轉起來派生類的一個實例,像這樣:

private bool ValidateLinkedField(string fieldName) 
    { 
     //loop through the "levels" (e.g. Order/Customer/Name) validating that the fields/properties all exist 
     Type currentType = ResultType; 
     foreach (string currentLevel in DeQualifyFieldName(fieldName, ResultType)) 
     { 
      MemberInfo match = (MemberInfo)currentType.GetField(currentLevel) ?? currentType.GetProperty(currentLevel); 
      if (match == null) return false; 
      currentType = match.MemberType == MemberTypes.Property ? ((PropertyInfo)match).PropertyType 
                    : ((FieldInfo)match).FieldType; 
     } 
     return true; //if we checked all levels and found matches, exit 
    } 

其餘所有的實現細節。如果您有興趣進行檢查,那麼包含完整實施的項目(包括測試數據)爲here。這是一個VS 2015項目,但如果這是一個問題,只需抓住Program.cs和Search.cs文件並將它們放入您選擇的IDE中的新項目。

感謝大家對StackOverflow的問題提出了問題並寫下了幫助我把這些問題放在一起的答案!