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…
August 21, 2009 at 7:21 am
[…] Introducing LazyLinq: Internals […]
August 21, 2009 at 7:25 am
[…] Introducing LazyLinq: Internals […]
September 12, 2009 at 10:03 pm
[…] Introducing LazyLinq: Internals […]
September 12, 2009 at 11:29 pm
[…] Introducing LazyLinq: Internals […]