С#, Linq2SQL:Создание предиката для поиска элементов в нескольких диапазонах

StackOverflow https://stackoverflow.com/questions/553443

Вопрос

Допустим, в моей базе данных есть что-то под названием 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;
}

Обратите внимание: я компилирую селектор перед его использованием.Это должно работать без проблем, я использовал что-то подобное в прошлом.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top