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:
- Introducing LazyLinq: Overview
- Introducing LazyLinq: Internals
- Introducing LazyLinq: Queryability
- Simplifying LazyLinq
- 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: