С#, Linq2SQL:Создание предиката для поиска элементов в нескольких диапазонах
-
23-08-2019 - |
Вопрос
Допустим, в моей базе данных есть что-то под названием Stuff со свойством Id.От пользователя я получаю последовательность выбранных объектов Range (точнее, я создаю их на основе их ввода) с нужными им идентификаторами.Урезанная версия этой структуры выглядит так:
public struct Range<T> : IEquatable<Range<T>>, IEqualityComparer<Range<T>>
{
public T A;
public T B;
public Range(T a, T b)
{
A = a;
B = b;
}
...
}
Так можно было бы, например, получить:
var selectedRange = new List<Range<int>>
{
new Range(1, 4),
new Range(7,11),
};
Затем я хочу использовать это для создания предиката для выбора только тех вещей, которые имеют значение между ними.Например, используя Построитель предикатов, я могу, например, сделать это так:
var predicate = PredicateBuilder.False<Stuff>();
foreach (Range<int> r in selectedRange)
{
int a = r.A;
int b = r.B;
predicate = predicate.Or(ø => ø.Id >= a && ø.Id <= b);
}
а потом:
var stuff = datacontext.Stuffs.Where(predicate).ToList();
Что работает!Сейчас я хотел бы создать универсальный метод расширения, который бы создавал для меня эти предикаты.Примерно так:
public static Expression<Func<T,bool>> ToPredicate<T>(this IEnumerable<Range<int>> range, Func<T, int> selector)
{
Expression<Func<T, bool>> p = PredicateBuilder.False<T>();
foreach (Range<int> r in range)
{
int a = r.A;
int b = r.B;
p = p.Or(ø => selector(ø) >= a && selector(ø) <= b);
}
return p;
}
Проблема здесь в том, что происходит сбой с NotSupportedException из-за вызова selector(ø): Method 'System.Object DynamicInvoke(System.Object[])' has no supported translation to SQL.
Думаю, это понятно.Но есть ли способ обойти это?В итоге я хотел бы просто сделать:
var stuff = datacontext.Stuffs.Where(selectedRange.ToPredicate<Stuff>(ø => ø.Id));
Или, что еще лучше, создайте что-то, что возвращает IQueryable, чтобы я мог просто сделать:
var stuff = datacontext.Stuffs.WhereWithin<Stuff>(selectedRange, ø => ø.Id); // Possibly without having to specify Stuff as type there...
Итак, есть идеи?Мне бы очень хотелось, чтобы это заработало, потому что в противном случае я получу МНОГО этих блоков кода foreach, создающих предикаты...
Примечание 1: Конечно, было бы неплохо, если бы я мог расшириться до большего, чем int, например DateTime и тому подобного, но не знаю, чем это закончится с использованием операторов >= и <=...Работает ли CompareTo с linq-to-sql?Если нет, нет проблем создать два.Один для int и один для DateTime, поскольку в основном это те типы, для которых он будет использоваться.
Заметка 2: Он будет использоваться для создания отчетов, где пользователь сможет сузить то, что получается, на основе разных факторов.Мол, мне нужен этот отчет для этих людей и этих дат.
Решение
Использование дженериков проблематично, поскольку C# не поддерживает операторы для дженериков, а это означает, что вам придется писать выражение вручную.И, как мы уже видели, строка работает по-другому.А в остальном, как насчет чего-то вроде (непроверенного):
(отредактировано для нескольких диапазонов)
public static IQueryable<TSource> WhereBetween<TSource, TValue>(
this IQueryable<TSource> source,
Expression<Func<TSource, TValue>> selector,
params Range<TValue>[] ranges)
{
return WhereBetween<TSource,TValue>(source, selector,
(IEnumerable<Range<TValue>>) ranges);
}
public static IQueryable<TSource> WhereBetween<TSource, TValue>(
this IQueryable<TSource> source,
Expression<Func<TSource, TValue>> selector,
IEnumerable<Range<TValue>> ranges)
{
var param = Expression.Parameter(typeof(TSource), "x");
var member = Expression.Invoke(selector, param);
Expression body = null;
foreach(var range in ranges)
{
var filter = Expression.AndAlso(
Expression.GreaterThanOrEqual(member,
Expression.Constant(range.A, typeof(TValue))),
Expression.LessThanOrEqual(member,
Expression.Constant(range.B, typeof(TValue))));
body = body == null ? filter : Expression.OrElse(body, filter);
}
return body == null ? source : source.Where(
Expression.Lambda<Func<TSource, bool>>(body, param));
}
Примечание;использование Expression.Invoke означает, что он, вероятно, будет работать с LINQ-to-SQL, но не с EF (на данный момент;надеюсь исправят в версии 4.0).
При использовании (проверено на Northwind):
Range<decimal?> range1 = new Range<decimal?>(0,10),
range2 = new Range<decimal?>(15,20);
var qry = ctx.Orders.WhereBetween(order => order.Freight, range1, range2);
Генерация TSQL (переформатирован):
SELECT -- (SNIP)
FROM [dbo].[Orders] AS [t0]
WHERE (([t0].[Freight] >= @p0) AND ([t0].[Freight] <= @p1))
OR (([t0].[Freight] >= @p2) AND ([t0].[Freight] <= @p3))
Именно то, что мы хотели ;-p
Другие советы
Вы получаете эту ошибку, потому что все для LINQ to SQL должно быть в форме Выражение.Попробуй это
public static Expression<Func<T,bool>> ToPredicate<T>(
this IEnumerable<Range<int>> range,
Expression<Func<T, int>> selector
) {
Expression<Func<T, bool>> p = PredicateBuilder.False<T>();
Func<T, int> selectorFunc = selector.Compile();
foreach (Range<int> r in range)
{
int a = r.A;
int b = r.B;
p = p.Or(ø => selectorFunc(ø) >= a && selectorFunc(ø) <= b);
}
return p;
}
Обратите внимание: я компилирую селектор перед его использованием.Это должно работать без проблем, я использовал что-то подобное в прошлом.