Introducing LazyLinq: Internals
This is the second 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
My first post introduced the three interfaces that LazyLinq provides. Next, we get to implement them.
Implementing ILazyQueryable
First, the interface:
public interface ILazyQueryable<TContext, TSource, TQuery>
: IQueryable<TSource>
where TQuery : IQueryable<TSource>
{
ILazyContext<TContext> Context { get; }
Func<TContext, TQuery> QueryBuilder { get; }
}
We’ll start simple with an implicit implementation of the interface and a trivial constructor:
class LazyQueryableImpl<TContext, TSource, TQuery>
: ILazyQueryable<TContext, TSource, TQuery>
where TQuery : IQueryable<TSource>
{
public ILazyContext<TContext> Context { get; private set; }
public Func<TContext, TQuery> QueryBuilder { get; private set; }
internal LazyQueryableImpl(ILazyContext<TContext> deferredContext, Func<TContext, TQuery> queryBuilder)
{
if (deferredContext == null) throw new ArgumentNullException("deferredContext");
if (queryBuilder == null) throw new ArgumentNullException("queryBuilder");
Context = deferredContext;
QueryBuilder = queryBuilder;
}
Next, a lazy-loaded query built from our lazy context:
protected TQuery Query
{
get
{
if (query == null)
{
query = QueryBuilder(Context.Context);
if (query == null)
throw new InvalidOperationException("Query built as null.");
}
return query;
}
}
And the internals of managing Context
, which implements IDisposable
:
private void Dispose()
{
Context.Dispose();
query = default(TQuery);
}
private IEnumerator<TSource> GetEnumerator()
{
try
{
foreach (var i in Query)
yield return i;
}
finally
{
Dispose();
}
}
Since Query
depends on Context
, once Context
is disposed we need to reset Query
so a new one can be built (if possible). Note that we use an iterator here to return an IEnumerator<TSource>
, rather than the usual IEnumerable<>
.
Finally, we’ll close out by explicitly implementing IQueryable
:
IEnumerator<TSource> IEnumerable<TSource>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
Type IQueryable.ElementType
{
get { return Query.ElementType; }
}
Expression IQueryable.Expression
{
get { return Query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return Query.Provider; }
}
}
If this seemed relatively simple, you’re right. We’re just building a lazy-loaded Query
proxy, with a bit of plumbing to clean up our Context
.
Implementing ILazyOrderedQueryable
Not very exciting, but for completeness:
public interface ILazyOrderedQueryable<TContext, TSource, TQuery>
: ILazyQueryable<TContext, TSource, TQuery>, IOrderedQueryable<TSource>
where TQuery : IOrderedQueryable<TSource>
{ }
class LazyOrderedQueryableImpl<TContext, TSource, TQuery>
: LazyQueryableImpl<TContext, TSource, TQuery>, ILazyOrderedQueryable<TContext, TSource, TQuery>
where TQuery : IOrderedQueryable<TSource>
{
internal LazyOrderedQueryableImpl(ILazyContext<TContext> lazyContext, Func<TContext, TQuery> queryBuilder)
: base(lazyContext, queryBuilder)
{
}
}
LazyQueryable Factory
Consumers of this API should never need to know about these implementation details, so we can hide them behind a factory class:
public static class LazyQueryable
{
public static ILazyQueryable<TContext, TResult, TQuery> CreateQuery<TContext, TResult, TQuery>(
ILazyContext<TContext> context, Func<TContext, TQuery> queryBuilder)
where TQuery : IQueryable<TResult>
{
return new LazyQueryableImpl<TContext, TResult, TQuery>(context, queryBuilder);
}
public static ILazyOrderedQueryable<TContext, TResult, TQuery> CreateOrderedQuery<TContext, TResult, TQuery>(
ILazyContext<TContext> context, Func<TContext, TQuery> queryBuilder)
where TQuery : IOrderedQueryable<TResult>
{
return new LazyOrderedQueryableImpl<TContext, TResult, TQuery>(context, queryBuilder);
}
}
Implementing ILazyContext
Again, we’ll start with the interface:
public interface ILazyContext<TContext> : IDisposable
{
TContext Context { get; }
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);
}
Now we can start fulfilling our requirements:
1. Lazily expose the Context.
class LazyContextImpl<TContext> : ILazyContext<TContext>, IDisposable
{
public Func<TContext> ContextBuilder { get; private set; }
private TContext context;
public TContext Context
{
get
{
if (context == null)
{
context = ContextBuilder();
if (context == null)
throw new InvalidOperationException("Context built as null.");
}
return context;
}
}
2. Produce lazy wrappers to represent queries retrieved from a context by a delegate.
public ILazyQueryable<TContext, TResult, TQuery> CreateQuery<TResult, TQuery>(
Func<TContext, TQuery> queryBuilder)
where TQuery : IQueryable<TResult>
{
return LazyQueryable.CreateQuery<TContext, TResult, TQuery>(this, queryBuilder);
}
public ILazyOrderedQueryable<TContext, TResult, TQuery> CreateOrderedQuery<TResult, TQuery>(
Func<TContext, TQuery> queryBuilder)
where TQuery : IOrderedQueryable<TResult>
{
return LazyQueryable.CreateOrderedQuery<TContext, TResult, TQuery>(this, queryBuilder);
}
3. Execute an action on the context.
There are two ways to “complete” a query, and we need to clean up context after each. The first was after enumeration, implemented above. The second is on execute, implemented here:
public TResult Execute<TResult>(Func<TContext, TResult> expression)
{
try
{
return expression(Context);
}
finally
{
Dispose();
}
}
4. Ensure the context is disposed as necessary.
We don’t require that TContext
is IDisposable
, but we need to handle if it is. We also clear context to support reuse.
public void Dispose()
{
var disposable = context as IDisposable;
if (disposable != null)
disposable.Dispose();
context = default(TContext);
}
Constructors
With our requirements met, we just need a way to create our context. We provide two options:
internal LazyContextImpl(TContext context) : this(() => context) { }
internal LazyContextImpl(Func<TContext> contextBuilder)
{
if (contextBuilder == null) throw new ArgumentNullException("contextBuilder");
ContextBuilder = contextBuilder;
}
The former wraps an existing TContext
instance in a closure, meaning every time ContextBuilder
is called it returns the same instance. The latter accepts any delegate that returns a TContext
. The most common such delegate would be a simple instantiation: () => new MyDataContext()
.
It should be clear now why we would want to clear our context on dispose. If ContextBuilder
returns a new context instance each time, it’s perfectly safe to discard of the old (disposed) context to trigger the creation of a new one. Conversely, if the builder returns a single instance, using the context after disposal would trigger an ObjectDisposedException
or something similar.
LazyContext Factory
For consistency, we should also provide factory methods to hide this specific implementation:
public static class LazyContext
{
public static ILazyContext<T> Create<T>(T context)
{
return new LazyContextImpl<T>(context);
}
public static ILazyContext<T> Create<T>(Func<T> contextBuilder)
{
return new LazyContextImpl<T>(contextBuilder);
}
}
Lazy Extensions
And last, but certainly not least, we’re ready to reimplement our Use()
extension methods:
public static class Lazy
{
public static ILazyContext<TContext> Use<TContext>(this TContext @this)
{
return LazyContext.Create<TContext>(@this);
}
public static ILazyContext<TContext> Use<TContext>(this Func<TContext> @this)
{
return LazyContext.Create<TContext>(@this);
}
}
With several usage possibilities:
var r1 = from x in new MyDataContext().Use() ...;
Func<MyDataContext> f1 = () => new MyDataContext();
var r2 = from x in f1.Use() ...;
var r3 = from x in new Func<MyDataContext>(() => new MyDataContext()).Use() ...;
var r4 = from x in Lazy.Use(() => new MyDataContext()) ...;
Or maybe we can make it even easier. Maybe…