解決方案
對於初學者來說,我們創造:
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的問題提出了問題並寫下了幫助我把這些問題放在一起的答案!