LINQ-to-SQL поиск по динамическим столбцам?

Используя пространство имен System.Linq.Dynamic, я могу создать общий список столбцов для поиска на основе столбцов, присутствующих в текущем пользовательском элементе управления (сетка с возможностью поиска, которую мы используем в разных местах). Процесс прост: возьмите список столбцов, видимый текущему пользователю, добавьте столбцы в выражение динамического запроса в предложении where, посмотрите, содержит ли вся объединенная последовательность указанную строку.

Этим достигается 2 вещи, позволяя пользователю выполнять поиск с помощью единого поля поиска (стиль Google), которое работает во всех сетках, которые пользователь видит одинаково, а также выгружая поиск в базу данных.

Вот как это работает сейчас (результат = IQueryable<T> или IEnumerable<T>):

var se = GetGridSearchExpression(grid);
if (se != null) result = result.Where(se, grid.SearchText.ToLower());

private static string GetGridSearchExpression(Grid grid)
{
  if (grid.SearchText.IsNullOrEmpty()) return null;
  var sb = new StringBuilder();
  foreach (var s in grid.ColumnNames)
  {
    sb.AppendFormat("{0} {1} ",
      sb.Length == 0 ? string.Empty : "+\"|^|\"+", s);
  }
  return string.Format("({0}).ToLower().Contains(@0)", sb);
}

Напечатанный "| ^ |" строка - это что-то случайное, чтобы предотвратить поиск в одном столбце, чтобы он соответствовал следующему, например столбцы «Bo» «Bryant» от соответствия поиску «Bob», результат поиска по запросу «Bo | ^ | Bryant» предотвращает совпадение.

Обнуляемые значения - вот где возникают проблемы, имея DateTime? или тип Nullable, например, приводит к следующей ошибке:

Expression of type 'System.Nullable`1[System.DateTime]' cannot be used for 
parameter of type 'System.Object' of method 
'System.String Concat(System.Object, System.Object)'

Вот часть DynamicQueryable, которая взорвалась:

Expression GenerateStringConcat(Expression left, Expression right) {
  return Expression.Call(null,
    typeof (string).GetMethod("Concat", new[] {typeof (object), typeof (object)}),
    new[] {left, right});
}

Единственный способ, который я нашел до сих пор, - это заменить добавление в построителе выражений на:

  foreach (var s in grid.ColumnNames)
  {
    sb.AppendFormat("{0}({1} != null ? {1}.ToString() : string.Empty)", 
      sb.Length == 0 ? string.Empty : "+\"|^|\"+", s);
  }

Поскольку мы находимся в LINQ to SQL, это приводит к раздутому оператору case. Учитывая альтернативу загрузки каждого объекта из БД для последующего поиска, можно использовать оператор case максимум для 8-10 столбцов.

Есть ли более чистый или простой способ сделать все или часть этого?

Отредактировано: Спасибо, Марк ... Я никогда не использую GetEnumerator где-либо в моем коде, всегда foreach или .ForEach () ... но по какой-то причине в то время это облегчило отладку, хотя я могу ' Теперь не помню почему. Решил вопрос по актуальному коду.


person Nick Craver    schedule 22.09.2009    source источник
comment
Кстати; Я не рекомендую вручную развертывать foreach - слишком легко ошибиться; например, вам не удалось избавиться от счетчика. Просто используйте foreach   -  person Marc Gravell    schedule 22.09.2009
comment
Отредактировано, чтобы показать работу с LINQ-to-Objects   -  person Marc Gravell    schedule 23.09.2009


Ответы (1)


Интересно, могли бы вы проверить Nullable<T> и использовать условное выражение? Но мне действительно интересно, не лучше ли было бы отказаться от динамической библиотеки LINQ; считать (непроверено):

строка [] columnNames = {"Имя", "DoB"}; строковый запрос = "2008";

        var row = Expression.Parameter(typeof(Data), "row");
        Expression body = null;
        Expression testVal = Expression.Constant(query, typeof(string));
        foreach (string columnName in columnNames)
        {
            Expression col = Expression.PropertyOrField(row, columnName);
            Expression colString = col.Type == typeof(string)
                ? col : Expression.Call(col, "ToString", null, null);
            Expression colTest = Expression.Call(
                colString, "Contains", null, testVal);

            if (col.Type.IsClass)
            {
                colTest = Expression.AndAlso(
                    Expression.NotEqual(
                        col,
                        Expression.Constant(null, typeof(string))
                    ),
                    colTest
                );
            }
            else if (Nullable.GetUnderlyingType(col.Type) != null)
            { // Nullable<T>
                colTest = Expression.AndAlso(
                    Expression.Property(col, "HasValue"),
                    colTest
                );
            }
            body = body == null ? colTest : Expression.OrElse(body, colTest);
        }
        Expression<Func<Data, bool>> predicate
            = body == null ? x => false : Expression.Lambda<Func<Data, bool>>(
                  body, row);


        var data = new[] {
            new Data { Name = "fred2008", DoB = null},
            new Data { Name = "barney", DoB = null},
            new Data { Name = null, DoB = DateTime.Today},
            new Data { Name = null, DoB = new DateTime(2008,1,2)}
        };
        foreach (Data x in data.AsQueryable().Where(predicate))
        {
            Console.WriteLine(x.Name + " / " + x.DoB);
        }

Тогда вы сможете использовать Where(predicate) в обычном LINQ; обратите внимание, что это не будет работать с LINQ-to-Objects (IEnumerable<T>) из-за значений NULL, но вероятно будет нормально в LINQ-to-SQL; если вам нужно, чтобы он работал и в LINQ-to-Objects, это нормально - просто нужно добавить еще несколько деталей к вышеизложенному (дайте мне знать).

person Marc Gravell    schedule 22.09.2009
comment
Иногда это работает, но не в двух случаях ... дочерние объекты, соединенные с LoadWith ‹T› (), генерируют указанный метод, который не поддерживается, в противном случае все, что допускает значение NULL (случай IEnumerable ‹T›), выдает нулевую ссылку ... сетки быть привязанным к анонимному типу в нескольких местах, так что повсюду есть смесь этих условий. Есть ли шанс, что эти 2 могут быть решены с помощью вашего подхода? Я бы хотел полностью отказаться от linq.dynamic, если это возможно ... если вам нужна дополнительная информация, дайте мне знать, я добавлю к вопросу. - person Nick Craver; 22.09.2009
comment
Я с нетерпением жду ваших идей ... перенос функции поиска в базу данных для самых худших на данный момент случаев производительности будет огромным и долгожданным улучшением. - person Nick Craver; 23.09.2009
comment
Спасибо, Марк, это отлично работает, добавлена ​​нечувствительность к регистру и хорошо подходит. На этом этапе я смог полностью удалить System.Linq.Dynamic из решения и по-прежнему получать все, что нам нужно, спасибо за помощь. - person Nick Craver; 24.09.2009