C #, Linq2SQL: создание предиката для поиска элементов в нескольких диапазонах - PullRequest
8 голосов
/ 16 февраля 2009

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

Затем я хочу использовать это для создания предиката, чтобы выбирать только те вещи, которые имеют значение между ними. Например, используя PredicateBuilder , я могу, например, сделать это следующим образом:

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 из-за вызова селектора (ø): 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, так как это в основном типы, для которых будет использоваться. </p>

Примечание 2: Он будет использоваться для составления отчетов, когда пользователь сможет сузить то, что выходит, основываясь на разных вещах. Мол, я хочу этот отчет для тех людей и тех дат.

Ответы [ 2 ]

7 голосов
/ 16 февраля 2009

Использование с обобщениями проблематично, поскольку 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

0 голосов
/ 16 февраля 2009

Вы получаете эту ошибку, потому что все для 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;
}

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

...