2016-02-11 3 views
1

Долгое время, решая эту проблему, я хотел поделиться решением.Генерировать многопараметрические запросы поиска LINQ с заданным временем возврата Тип

фон

Я поддерживать большой веб-приложения с основной функцией управления заказами. Это приложение MVC поверх C#, использующее EF6 для данных.

Существует много поисковых экранов. Все экраны поиска имеют несколько параметров и возвращают разные типы объектов.

Проблема

Каждого экрана поиск был:

  • ViewModel с параметрами поиска
  • метода
  • контроллера для обработки поиска событий
  • метода, чтобы вытащить правильные данные для этого экрана
  • Способ применения всех фильтров поиска к набору данных
  • метод для преобразования результатов в новые результаты ViewModel
  • Полученных результатов ViewModel

Это добавляет быстро. У нас есть около 14 различных экранов поиска, что означает около 84 моделей & методов обработки этих запросов.

Моя цель

Я хотел, чтобы иметь возможность создать класс, аналогичный текущего параметра поиска ViewModel, который унаследует от базового класса 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 означает, что имя поля поиска совпадает с именем поля целевого объекта.

С этим сконфигурировано, единственными, что мне нужно для данной категории является:

  • населенного поиска объект, как один выше
  • источника данных

Нет другой от сценария требуется специальное кодирование!

ответ

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

Для каждого поле поиска, нам нужно знать не только то, что выполняется, но и 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, который задал вопросы и написал ответы, которые помогли мне собрать это вместе!

Смежные вопросы