C#,Linq2SQL:创建一个谓词来查找在若干范围内的元素

8
假设我在数据库中有一个名为“Stuff”的东西,其中包含一个名为“Id”的属性。从用户那里,我得到了一系列所选的范围对象(或者更确切地说,我从他们的输入中创建了这些对象),这些对象带有他们想要的Id。该结构的简化版本如下:
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崩溃:方法“System.Object DynamicInvoke(System.Object[])”没有支持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:它将用于报告,用户将能够根据不同的条件缩小输出范围。例如,我想要这份报告关于那些人和日期。

2个回答

7
使用通用泛型存在问题,因为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。

那会如何与一整个 Range<TValue> 对象系列一起工作? - Svish
你的参数中的"x"是什么? - Svish
表达式参数需要命名。如果我们手写 lambda 表达式,它将是 "x => x.Val < 1 && x.Val > 2" 中的 "x" - 它仅与正在处理的行相关。如果你不喜欢,也许可以称之为 "row"。;-p - Marc Gravell
啊哈,很酷。不,不,我只是不知道它的用途是什么 :p 你现在改变的东西能够与 IEnumerable<Range<TValue>> 和 Range<TValue>[] 一起使用吗? - Svish
使用foreach循环,而不是for(int i...这样的语句? - Svish

0

你之所以会收到这个错误,是因为所有与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;
}

请注意,在使用选择器之前,我会先编译它。这应该可以顺利运行,因为我以前也用过类似的方法。

那么我该如何使用选择器呢?换句话说,在 p = p.Or(ø => selector(ø) >= a && selector(ø) <= b); 中,我用什么替换 selector(ø) 呢? - Svish
不,当我这样做时,我得到相同的“方法'System.Object DynamicInvoke(System.Object[])'没有受支持的SQL翻译。” - Svish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接