C#,Linq2SQL:创建谓词以查找多个范围内的元素

Svi*_*ish 8 c# generics extension-methods predicate linq-to-sql

假设我的数据库中有一个名为Stuff的东西,名为Id.从用户那里我得到一系列选定的Range对象(或者我是从输入中创建它们)和他们想要的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;
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

所以有人可以得到:

var selectedRange = new List<Range<int>>
    {
        new Range(1, 4),
        new Range(7,11),
    };
Run Code Online (Sandbox Code Playgroud)

然后我想用它来创建一个谓词,只选择那些之间具有值的东西.例如,使用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);
}
Run Code Online (Sandbox Code Playgroud)

然后:

var stuff = datacontext.Stuffs.Where(predicate).ToList();
Run Code Online (Sandbox Code Playgroud)

哪个有效!我现在想做的是创建一个通用的扩展方法来为我创建这些谓词.有点像这样:

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;
}
Run Code Online (Sandbox Code Playgroud)

这里的问题是,由于选择器(ø)调用它崩溃了NotSupportedException: Method 'System.Object DynamicInvoke(System.Object[])' has no supported translation to SQL.

我想这是可以理解的.但有什么方法可以解决这个问题吗?我想最终得到的是我可以做到:

var stuff = datacontext.Stuffs.Where(selectedRange.ToPredicate<Stuff>(ø => ø.Id));
Run Code Online (Sandbox Code Playgroud)

或者甚至更好,创建一些返回IQueryable的东西,这样我就可以:

var stuff = datacontext.Stuffs.WhereWithin<Stuff>(selectedRange, ø => ø.Id); // Possibly without having to specify Stuff as type there...
Run Code Online (Sandbox Code Playgroud)

那么,有什么想法吗?我真的想让这个工作,因为如果不是我会得到很多那些foreach代码块,创建谓词...


注1:当然,如果我可以扩展到超过int,比如DateTime等,但是不确定如何使用> =和<=运算符会更好...比较是否可以使用linq-to-的sql?如果不是,创建两个没有问题.一个用于int,一个用于DateTime,因为这主要是将用于的类型.

注2:它将用于报告,用户将能够根据不同的事情缩小出来的范围.就像,我想要这些人和那些日期的报告.

Mar*_*ell 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));
    }
Run Code Online (Sandbox Code Playgroud)

注意; 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);
Run Code Online (Sandbox Code Playgroud)

生成TSQL(重新格式化):

SELECT -- (SNIP)
FROM [dbo].[Orders] AS [t0]
WHERE (([t0].[Freight] >= @p0) AND ([t0].[Freight] <= @p1))
OR (([t0].[Freight] >= @p2) AND ([t0].[Freight] <= @p3))
Run Code Online (Sandbox Code Playgroud)

正是我们想要的;-p