Решение
Для начала, мы создаем:
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;
}
}
Для каждого поле поиска, нам нужно знать не только то, что выполняется, но и WHUTHER, поиск выполнен. Например, если значение «TxNumber» равно null, мы не хотим запускать этот поиск. Таким образом, мы создаем объект 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;
}
}
После того, как мы создали все наши фильтры, все, что нам нужно сделать, это цикл через них и вызвать метод «Применить» на нашем наборе данных! Легко!
Следующий шаг - создание выражений валидации. Мы сделаем это на основе типа; каждый 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. Или, по крайней мере, я надеюсь, что нет. :) Это не точно, но я счел, что лучше принимать и исправлять полностью квалифицированные имена объектов, чем поддерживать этот край.
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;
}
Это просто прямой текст синтаксического анализа, и возвращает имя поля в «уровни» (например, «Клиент» | «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 - заказ, в данном случае. Сначала мы создаем параметр t. Затем мы прокручиваем части имени свойства/поля, пока у нас не будет полного названия объекта, на который мы нацеливаемся (называя его «левым», потому что это левая сторона нашего сравнения). «Правая» сторона нашего сравнения проста: константа, предоставленная пользователем.
Затем мы создаем двоичное выражение и превращаем его в лямбду. Легко, как падение с журнала! Во всяком случае, если сваливать журнал, требуются бесчисленные часы разочарования и неудачных методологий. Но я отвлекся.
Теперь у нас есть все части; все, что нам нужно, это способ собрать наш запрос:
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 из них, и применять их.
Clean-Up
Там немного больше, конечно. Например, мы указываем ссылки на объекты со строками. Что делать, если есть опечатка?
В моем случае, у меня есть чек класса всякого раза, когда он раскручивается экземпляр производного класса, например:
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, который задал вопросы и написал ответы, которые помогли мне собрать это вместе!