Simplifying LazyLinq
This is the fourth 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
As I was iterating on the proof of concept for LazyLinq, I always wanted to get rid of the TQuery type parameter. I thought I needed it to distinguish between ordered and unordered wrapped queries, but it just felt messy. The underlying provider mechanism didn’t need it, so why should I?
Well after taking a closer look at the SQL query provider, I figured out how to eliminate it. The object of inspiration is System.Data.Linq.DataQuery<T>, defined as follows:
internal sealed class DataQuery<T> :
IOrderedQueryable<T>, IQueryable<T>,
IOrderedQueryable, IQueryable,
IEnumerable<T>, IEnumerable,
IQueryProvider,
IListSource
The key was realizing that IOrderedQueryable<> and ILazyOrderedQueryable<> don’t actually do anything. Implementation-wise, they’re just IQueryable<> or ILazyQueryable<> with an extra interface on top. It’s only on the design side that it actually matters, essentially providing a hook for additional ordering with ThenBy. In LINQ to SQL’s case, that means supporting orderability is as simple as specifying that the query object is both IQueryable<> and IOrderedQueryable<>.
So how does this revelation simplify Lazy LINQ? First, it allows us to remove TQuery from the interfaces:
public interface ILazyContext<TContext> : IDisposable
{
TContext Context { get; }
ILazyQueryable<TContext, TResult>
CreateQuery<TResult>(Func<TContext, IQueryable<TResult>> queryBuilder);
TResult Execute<TResult>(Func<TContext, TResult> action);
}
public interface ILazyQueryable<TContext, TSource> : IQueryable<TSource>
{
ILazyContext<TContext> Context { get; }
Func<TContext, IQueryable<TSource>> QueryBuilder { get; }
}
public interface ILazyOrderedQueryable<TContext, TSource>
: ILazyQueryable<TContext, TSource>, IOrderedQueryable<TSource>
{ }
Note that we can also eliminate ILazyContext.CreateOrderedQuery(), instead assuming that CreateQuery() will return something that can be treated as ILazyOrderedQueryable<> as necessary.
For the concrete implementations, we take the cue from DataQuery<T>, letting LazyQueryableImpl implement ILazyOrderedQueryable<> so we can eliminate LazyOrderedQueryableImpl:
class LazyQueryableImpl<TContext, TSource>
: ILazyQueryable<TContext, TSource>, ILazyOrderedQueryable<TContext, TSource>
{
// Implementation doesn't change
}
Finally, our sorting query operations will look more like their counterparts in System.Linq.Queryable, casting the result of CreateQuery() to ILazyOrderedQueryable<>. To keep things readable, we’ll split our CreateOrderedQuery<> helper into separate versions for OrderBy and ThenBy. Note how the types of queryOperation map to the usage of OrderBy (unordered to ordered) and ThenBy (ordered to ordered):
private static ILazyOrderedQueryable<TContext, TResult>
CreateOrderByQuery<TSource, TContext, TResult>(
this ILazyQueryable<TContext, TSource> source,
Func<IQueryable<TSource>, IOrderedQueryable<TResult>> queryOperation
)
{
return (ILazyOrderedQueryable<TContext, TResult>) source.Context.CreateQuery<TResult>(
context => queryOperation(source.QueryBuilder(context)));
}
private static ILazyOrderedQueryable<TContext, TResult>
CreateThenByQuery<TSource, TContext, TResult>(
this ILazyQueryable<TContext, TSource> source,
Func<IOrderedQueryable<TSource>, IOrderedQueryable<TResult>> queryOperation
)
{
return (ILazyOrderedQueryable<TContext, TResult>) source.Context.CreateQuery<TResult>(
context => queryOperation((IOrderedQueryable<TSource>) source.QueryBuilder(context)));
}
Removing TQuery from the query operators is left as an exercise for the reader. Or you can just get the source on CodePlex.
A Note on IOrderedEnumerable<>
Having taken advantage of how LINQ to IQueryable handles orderability, it’s worth pointing out that LINQ to Objects uses a different approach, specifying new behavior in IOrderedEnumerable<> that is used to support multiple sort criteria:
public interface IOrderedEnumerable<TElement> : IEnumerable<TElement>, IEnumerable
{
IOrderedEnumerable<TElement>
CreateOrderedEnumerable<TKey>(
Func<TElement, TKey> keySelector,
IComparer<TKey> comparer, bool descending);
}
