Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C#, Linq2Sql: Is it possible to concatenate two queryables into one?

I have one queryable where I have used various Where and WhereBetween statements to narrow the collection down to a certain set. Now I need to add kind of a Where || WhereBetween. In other words, I can't just chain them together like I have up till now, cause that will work as an And. So, how can I do this?

I see two possibilities:

  1. Create two queryables from the one I have, one using the Where, and one using WhereBetween. And then concatenate them. Don't know if this is even possible? Also, although not in my particular case, you would most likely end up with duplicates...
  2. Somehow merge the Where expression and the expression created in the WhereBetween with some sort of Or.

The first, as mentioned, I am not sure is even possible. And if it was, I'm not so sure it is a good way to do it.

The second, I can see as an option, but not totally sure about all the details. Below is the WhereBetween method from my other question, which I now use and it works great:

    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));
    }

I'm thinking that I could maybe extract the expression building portion of it into a new method. Perhaps like this:

    public static IQueryable<TSource> WhereBetween<TSource, TValue>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TValue>> selector,
        IEnumerable<Range<TValue>> ranges)
    {
        return source.Where(WhereBetween(selector, ranges));
    }

    public static Expression<Func<TSource, bool>> WhereBetween<TSource, TValue>(
        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 
            ? ø => true
            : Expression.Lambda<Func<TSource, bool>>(body, param);
    }

I could then use that new method to get the expression instead of the queryable. So, lets say I have the WhereBetween(ø => ø.Id, someRange) and for example ø => ø.SomeValue == null. How can I combine those two with Or? I'm looking at the Expression.OrElse used in the WhereBetween method, and I think that might be what I need, or maybe this the Expression.Or. But I'm very unstable on this expression stuff, so I am not sure what to choose here, or even if I am on the right track :p

Could someone give me some pointers here?

like image 657
Svish Avatar asked Feb 20 '09 14:02

Svish


1 Answers

You have two options here - Queryable.Union, or expression combination. I'd generally favor the latter, via OrElse - which (with LINQ-to-SQL at least) you can do with 2 expressions (see below) - but in either case it should get composed:

    using(var ctx = new DataClasses1DataContext())
    {
        ctx.Log = Console.Out;
        Expression<Func<Customer, bool>> lhs =
            x => x.Country == "UK";
        Expression<Func<Customer, bool>> rhs =
            x => x.ContactName.StartsWith("A");

        var arr1 = ctx.Customers.Where(
            lhs.OrElse(rhs)).ToArray();

        var arr2 = ctx.Customers.Where(lhs)
            .Union(ctx.Customers.Where(rhs)).ToArray();
    }

Both arr1 and arr2 each only perform 1 database hit (although the TSQL is different; the first has an OR in the WHERE clause; the second has two separate queries with UNION).

Here's the extension method I used:

static Expression<Func<T, bool>> OrElse<T>(
    this Expression<Func<T, bool>> lhs,
    Expression<Func<T, bool>> rhs)
{
    var row = Expression.Parameter(typeof(T), "row");
    var body = Expression.OrElse(
        Expression.Invoke(lhs, row),
        Expression.Invoke(rhs, row));
    return Expression.Lambda<Func<T, bool>>(body, row);
}
like image 149
Marc Gravell Avatar answered Oct 05 '22 23:10

Marc Gravell