Code First

A More General Queryable Collection

In the last three posts we looked at an implementation of extra-lazy Count for EF 4.1 and how to reduce the Reflection cost of this implementation. However, when looking at LazyCountCollection it is fairly apparent that the same pattern can be used for more than just extra-lazy Count. In this we’ll look at a more general implementation of ICollection<T> that contains an underlying IQueryable<T> that can be used for more than just extra-lazy Count.

LazyCountCollection is an ICollection<T> implementation that delegates most operations to another underlying ICollection<T> but also contains a DbCollectionEntry that is occasionally used instead of the underlying collection. However, the only part of DbCollectionEntry that we really need is the Query method, which returns an IQueryable<T>. Therefore, we can make our implementation more general by working only with IQueryable<T> instead of the DbCollectionEntry. Here’s the code:

namespace LazyUnicorns
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Linq.Expressions;

    public class QueryableCollection<T>
        : ICollection<T>, IQueryable<T>, IHasIsLoaded
    {
        private readonly ICollection<T> _collection;
        private readonly IQueryable<T> _query;

        public QueryableCollection(ICollection<T> collection, IQueryable<T> query)
        {
            _collection = collection ?? new HashSet<T>();
            _query = query;
        }

        public IQueryable<T> Query
        {
            get { return _query; }
        }

        public bool IsLoaded { get; set; }

        public void Add(T item)
        {
            _collection.Add(item);
        }

        public void Clear()
        {
            LazyLoad();
            _collection.Clear();
        }

        public bool Contains(T item)
        {
            LazyLoad();
            return _collection.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            LazyLoad();
            _collection.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get
            {
                return IsLoaded ? _collection.Count : _query.Count();
            }
        }

        public bool IsReadOnly
        {
            get
            {
                return _collection.IsReadOnly;
            }
        }

        public bool Remove(T item)
        {
            LazyLoad();
            return _collection.Remove(item);
        }

        public IEnumerator<T> GetEnumerator()
        {
            LazyLoad();
            return _collection.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            // Can't call LazyLoad here due to bug in EF, but usually when writing
            // code the generic enumerator is called anyway.
            return ((IEnumerable)_collection).GetEnumerator();
        }

        private void LazyLoad()
        {
            if (!IsLoaded)
            {
                IsLoaded = true;
                _query.Load();
            }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }
    }
}

And here are an interface and extension methods for using IsLoaded:

namespace LazyUnicorns
{
    public interface IHasIsLoaded
    {
        bool IsLoaded { get; set; }
    }
}

namespace LazyUnicorns
{
    using System.Collections.Generic;

    public static class CollectionExtensions
    {
        public static bool IsLoaded<T>(this ICollection<T> collection)
        {
            var asHasIsLoaded = collection as IHasIsLoaded;
            return asHasIsLoaded != null ? asHasIsLoaded.IsLoaded : true;
        }

        public static void SetLoaded<T>(this ICollection<T> collection, bool isLoaded)
        {
            var asHasIsLoaded = collection as IHasIsLoaded;
            if (asHasIsLoaded != null)
            {
                asHasIsLoaded.IsLoaded = isLoaded;
            }
        }
    }
}

For QueryableCollection, instead of passing a DbCollectionEntry to the constructor we now pass an IQueryable<T>. We can then use this query directly in the Count property.

In LazyCountCollection we were also using the Load method of DbCollectionEntry to do lazy loading of the collection. This has been replaced with a call to the Load extension method on the query. This extension method is defined in the System.Data.Entity namespace (and lives in EntityFramework.dll) but is not actually tied to the Entity Framework in any other way. It does is the same thing as ToList but without actually creating the list. If you wanted to remove all uses of EF from this class you could replace Load with ToList, or write your own Load method.

Creating QueryableCollections

We can use QueryableCollection in our entities by creating a new concrete implementation of CachingCollectionInitializer:

namespace LazyUnicorns
{
    using System.Collections.Generic;
    using System.Data.Entity.Infrastructure;
    using System.Linq;

    public class QueryableCollectionInitializer : CachingCollectionInitializer
    {
        public override object CreateCollection<TElement>(DbCollectionEntry collectionEntry)
        {
            return new QueryableCollection<TElement>(
                (ICollection<TElement>)collectionEntry.CurrentValue,
                collectionEntry.Query().Cast<TElement>());
        }
    }
}

We can create an instance of this initializer and use it with the ObjectMaterialized event just as in our previous posts.

Some Tests…

All of the existing tests we had for LazyCountCollection will still pass with this new, more general, implementation. In addition, here are a few more tests that demonstrate some of the things you can do with the new implementation:

[TestMethod]
public void QueryableCollection_can_be_used_for_First_without_loading_entire_collection()
{
    using (var context = new BlogContext())
    {
        var post = context.Posts.Find(1);

        var firstComment = post.Comments
            .AsQueryable()
            .OrderBy(c => c.Id)
            .FirstOrDefault();

        Assert.IsNotNull(firstComment);
        Assert.AreEqual(1, context.ChangeTracker.Entries<Comment>().Count());
    }
}

[TestMethod]
public void QueryableCollection_can_be_used_to_load_filtered_results()
{
    using (var context = new BlogContext())
    {
        var post = context.Posts.Find(1);

        var unicornComments = post.Comments
            .AsQueryable()
            .Where(c => c.Content.Contains("unicorn"))
            .ToList();

        Assert.AreEqual(2, unicornComments.Count());
        Assert.AreEqual(2, context.ChangeTracker.Entries<Comment>().Count());
    }
}

[TestMethod]
public void IHasIsLoaded_can_be_used_to_set_IsLoaded_after_a_filtered_query()
{
    using (var context = new BlogContext())
    {
        var post = context.Posts.Find(1);

        post.Comments.AsQueryable()
            .Where(c => c.Content.Contains("unicorn"))
            .Load();
        post.Comments.SetLoaded(true);

        Assert.AreEqual(2, post.Comments.Count); // Doesn't trigger further loading
        Assert.AreEqual(2, context.ChangeTracker.Entries<Comment>().Count());
    }
}