Extra-Lazy Collection Count with EF 4.1 (Part 2)

Using LazyCountCollection in a model

In part 1 we setup all the infrastructure for implementing an extra-lazy Count property with EF 4.1—now let’s actually use it!

I’m going to be boring and just use the new Northwind—a blogging model. Here are my entities and context implementation:

namespace LazyUnicornTests.Model
{
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    using LazyUnicorns;

    public class BlogContext : DbContext
    {
        public BlogContext()
        {
            Database.SetInitializer(new BlogsContextInitializer());
            Configuration.LazyLoadingEnabled = false;

            ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
                (s, e) => new LazyCountCollectionInitializer()
                    .InitializeCollections(this, e.Entity);
        }

        public DbSet<Post> Posts { get; set; }
        public DbSet<Comment> Comments { get; set; }
    }
}

namespace LazyUnicornTests.Model
{
    public class Comment
    {
        public int Id { get; set; }
        public string Content { get; set; }
        public Post Post { get; set; }
    }
}

namespace LazyUnicornTests.Model
{
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;

    public class Post
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public ICollection<Comment> Comments { get; set; }
    }
}

The most interesting point to notice about the code above is that the constructor registers an instance of LazyCountCollectionInitializer with the ObjectMaterialized event of the underlying ObjectContext. Notice that the entities themselves just have simple ICollection<T> properties and don’t explicitly reference LazyCountCollection.

Everything else is pretty straightforward EF 4.1 code so I won’t go into more details here.

Some tests…

Rather than demonstrate the code through an application I have instead written some tests. The model above is for use with those tests and hence it is setup more as a test model than a real model, including doing things like calling SetInitializer in the context constructor.

The tests are mostly not real unit tests, but rather small functional tests for certain expected behaviors of the code. Real unit tests would not create a real database and execute real queries against it. Nevertheless, this type of small functional test is very easy to write with EF 4.1 and serves as an easy way to test behavior with little code.

The tests make use of a straightforward database initializer that adds some sample blogs and comments when the database is created. The assertions in the tests rely on this well-known data being present in the database. Here is the initializer code:

namespace LazyUnicornTests.Model
{
    using System.Collections.Generic;
    using System.Data.Entity;

    public class BlogsContextInitializer : DropCreateDatabaseIfModelChanges<BlogContext>
    {
        protected override void Seed(BlogContext context)
        {
            context.Posts.Add(new Post
            {
                Id = 1,
                Title = "Lazy Unicorns",
                Comments = new List<Comment>
                {
                    new Comment { Content = "Are enums supported?" },
                    new Comment { Content = "My unicorns are so lazy they fell asleep." },
                    new Comment { Content = "Is a unicorn without a horn just a horse?" },
                }
            });

            context.Posts.Add(new Post
            {
                Id = 2,
                Title = "Sleepy Horses",
                Comments = new List<Comment>
                {
                    new Comment { Content = "Are enums supported?" },
                }
            });
        }
    }
}

And here are the tests:

namespace LazyUnicornTests
{
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using LazyUnicorns;
    using LazyUnicornTests.Model;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

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

                Assert.AreEqual(3, post.Comments.Count);
                Assert.AreEqual(0, context.ChangeTracker.Entries<Comment>().Count());
            }
        }

        [TestMethod]
        public void LazyCountCollection_Count_returns_count_even_when_collection_is_loaded()
        {
            using (var context = new BlogContext())
            {
                var post = context.Posts.Find(1);
                context.Entry(post).Collection(p => p.Comments).Load();

                Assert.AreEqual(3, post.Comments.Count);
                Assert.AreEqual(3, context.ChangeTracker.Entries<Comment>().Count());
            }
        }

        [TestMethod]
        public void LazyCountCollection_Count_returns_database_count_not_collection_count()
        {
            using (var context = new BlogContext())
            {
                var post = context.Posts.Find(1);
                context.Entry(post).Collection(p => p.Comments).Load();
                post.Comments.Add(new Comment());

                Assert.AreEqual(3, post.Comments.Count);
                Assert.AreEqual(4, context.ChangeTracker.Entries<Comment>().Count());
            }
        }

        [TestMethod]
        public void Enumerating_the_LazyCountCollection_causes_it_to_be_lazy_loaded()
        {
            using (var context = new BlogContext())
            {
                context.Posts.Find(1).Comments.ToList();

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

        [TestMethod]
        public void Adding_to_the_LazyCountCollection_does_not_cause_it_to_be_lazy_loaded()
        {
            using (var context = new BlogContext())
            {
                context.Posts.Find(1).Comments.Add(new Comment());

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

        [TestMethod]
        public void LazyCountCollection_Count_returns_count_even_when_collection_is_eager_loaded()
        {
            using (var context = new BlogContext())
            {
                var post = context.Posts
                    .Where(p => p.Id == 1)
                    .Include(p => p.Comments)
                    .Single();

                Assert.AreEqual(3, post.Comments.Count);
                Assert.AreEqual(3, context.ChangeTracker.Entries<Comment>().Count());
            }
        }

        public class FakeEntityWithListCollection
        {
            public List<Post> Posts { get; set; }
        }

        [TestMethod]
        public void Collections_not_declared_as_ICollection_are_ignored()
        {
            Assert.IsNull(new LazyCountCollectionInitializer()
                .TryGetElementType(typeof(FakeEntityWithListCollection)
                .GetProperty("Posts")));
        }

        public class FakeEntityWithReadonlyCollection
        {
            public ICollection<Post> Posts { get { return null; } }
        }

        [TestMethod]
        public void Collections_without_setters_are_ignored()
        {
            Assert.IsNull(new LazyCountCollectionInitializer()
                .TryGetElementType(typeof(FakeEntityWithReadonlyCollection)
                .GetProperty("Posts")));
        }

    }
}