Introducing LazyLinq: Queryability

This is the third in a series of posts on LazyLinq, a wrapper to support lazy initialization and deferred disposal of a LINQ query context:

  1. Introducing LazyLinq: Overview
  2. Introducing LazyLinq: Internals
  3. Introducing LazyLinq: Queryability
  4. Simplifying LazyLinq
  5. Introducing LazyLinq: Lazy DataContext

Having defined the LazyLinq interfaces and provided concrete implementations, we’re left to provide support for the standard query operators.

Learning from Queryable

Before we try to query ILazyQueryable, it’s instructive to look at how System.Linq.Queryable works. There are essentially three types of operators on IQueryable<>:

  • Deferred queries returning IQueryable<>: Select, Where, etc.
  • Deferred query returning IOrderedQueryable<>: OrderBy, OrderByDescending, ThenBy, ThenByDescending
  • Everything else: Aggregate, Count, First, etc.

Reflecting Queryable.Select, modulo error checking, we see the following:

public static IQueryable<TResult> Select<TSource, TResult>(
    this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
{
    return source.Provider
        .CreateQuery<TResult>(
            Expression.Call(null,
                ((MethodInfo) MethodBase.GetCurrentMethod())
                    .MakeGenericMethod(new Type[] { typeof(TSource), typeof(TResult) }),
                new Expression[] { source.Expression, Expression.Quote(selector) }));
}

The source‘s Provider is used to construct a new query whose expression includes the call to Select with the given parameters. An ordered query follows a similar pattern, trusting that the query provider will return an IOrderedQueryable<> as appropriate:

public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(
    this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector)
{
    return (IOrderedQueryable<TSource>) source.Provider
        .CreateQuery<TSource>(
            Expression.Call(null,
                ((MethodInfo) MethodBase.GetCurrentMethod())
                    .MakeGenericMethod(new Type[] { typeof(TSource), typeof(TKey) }),
                new Expression[] { source.Expression, Expression.Quote(keySelector) }));
}

And finally, everything that’s not a query is handled by the provider’s Execute method:

public static TSource First<TSource>(this IQueryable<TSource> source)
{
    return source.Provider
        .Execute<TSource>(
            Expression.Call(null,
                ((MethodInfo) MethodBase.GetCurrentMethod())
                    .MakeGenericMethod(new Type[] { typeof(TSource) }),
                new Expression[] { source.Expression }));
}

Querying ILazyQueryable

You may have noticed that the above scenarios map rather closely to the methods provided by ILazyContext:

  • ILazyQueryable<TContext, TResult, TQuery>
        CreateQuery<TResult, TQuery>(Func<TContext, TQuery> queryBuilder)
        where TQuery : IQueryable<TResult>;
  • ILazyOrderedQueryable<TContext, TResult, TQuery>
        CreateOrderedQuery<TResult, TQuery>(Func<TContext, TQuery> queryBuilder)
        where TQuery : IOrderedQueryable<TResult>;
  • TResult Execute<TResult>(Func<TContext, TResult> action);

However, rather than expression trees we’re pushing around delegates. Execute seems pretty simple, so let’s start there:

public static TSource First<TSource, TContext, TQuery>(
        this ILazyQueryable<TContext, TSource, TQuery> source
    ) where TQuery : IQueryable<TSource>
{
    Func<TContext, TResult> action = context => ???;
    return source.Context.Execute(action);
}

So our source has a Context, which knows how to Execute an action from context to some result. To find that result, we need to leverage the other property of source: QueryBuilder. Recalling that QueryBuilder is a function from TContext to TQuery, and that TQuery is an IQueryable<TSource>, we see something on which we can execute Queryable.First:

public static TSource First<TSource, TContext, TQuery>(
        this ILazyQueryable<TContext, TSource, TQuery> source
    ) where TQuery : IQueryable<TSource>
{
    Func<TContext, TResult> action =
        context => source.QueryBuilder(context).First();
    return source.Context.Execute(action);
}

Now seeing as we have dozens of methods to implement like this, it seems an opportune time for a bit of eager refactoring. Recognizing that the only variance is the method call on the IQueryable, let’s extract an extension method that does everything else:

private static TResult Execute<TSource, TContext, TResult, TQuery>(
        this ILazyQueryable<TContext, TSource, TQuery> source,
        Func<TQuery, TResult> queryOperation
    ) where TQuery : IQueryable<TSource>
{
    return source.Context.Execute(
        context => queryOperation(source.QueryBuilder(context)));
}

From there, additional lazy operators are just a lambda expression away:

public static TSource First<TSource, TContext, TQuery>(
        this ILazyQueryable<TContext, TSource, TQuery> source
    ) where TQuery : IQueryable<TSource>
{
    return source.Execute(q => q.First());
}

public static TAccumulate Aggregate<TContext, TSource, TQuery, TAccumulate>(
        this ILazyQueryable<TContext, TSource, TQuery> source,
        TAccumulate seed, Expression<Func<TAccumulate, TSource, TAccumulate>> func
    ) where TQuery : IQueryable<TSource>
{
    return source.Execute(q => q.Aggregate(seed, func));
}

And now having done the hard part (finding an IQueryable), we can translate that understanding to make similar helpers for queries:

private static ILazyQueryable<TContext, TResult, IQueryable<TResult>>
    CreateQuery<TSource, TContext, TQuery, TResult>(
        this ILazyQueryable<TContext, TSource, TQuery> source,
        Func<TQuery, IQueryable<TResult>> queryOperation
    ) where TQuery : IQueryable<TSource>
{
    return source.Context.CreateQuery<TResult, IQueryable<TResult>>(
        context => queryOperation(source.QueryBuilder(context)));
}

private static ILazyOrderedQueryable<TContext, TResult, IOrderedQueryable<TResult>>
    CreateOrderedQuery<TSource, TContext, TQuery, TResult>(
        this ILazyQueryable<TContext, TSource, TQuery> source,
        Func<TQuery, IOrderedQueryable<TResult>> queryOperation
    ) where TQuery : IQueryable<TSource>
{
    return source.Context.CreateOrderedQuery<TResult, IOrderedQueryable<TResult>>(
        context => queryOperation(source.QueryBuilder(context)));
}

With similarly trivial query operator implementations:

public static ILazyQueryable<TContext, TResult, IQueryable<TResult>>
    Select<TContext, TSource, TQuery, TResult>(
        this ILazyQueryable<TContext, TSource, TQuery> source,
        Expression<Func<TSource, TResult>> selector
    ) where TQuery : IQueryable<TSource>
{
    return source.CreateQuery(q => q.Select(selector));
}

public static ILazyOrderedQueryable<TContext, TSource, IOrderedQueryable<TSource>>
    OrderBy<TContext, TSource, TQuery, TKey>(
        this ILazyQueryable<TContext, TSource, TQuery> source,
        Expression<Func<TSource, TKey>> keySelector
    ) where TQuery : IQueryable<TSource>
{
    return source.CreateOrderedQuery(q => q.OrderBy(keySelector));
}

And the end result:

LazyLinq.First

Follow

Get every new post delivered to your Inbox.