EF CTP4 Tips & Tricks: Querying Navigations Without Loading

This is the fifth in a series of posts about the recently released Entity Framework Feature CTP4, now affectionately known as “EF Magic Unicorn Edition”.

Today we are going to take a look at how to query the contents of the navigation property of a loaded entity without loading the contents of that navigation into memory. For folks who are familiar with the default code generation in EF, you may have used the CreateSourceQuery method on EntityReference and EntityCollection to achieve this functionality.

The Problem

In the following console app we have a pretty simple model defined and we’ve made the navigation properties virtual so that we get lazy loading. There is however some room for improvement in our Main method, first off we are pulling an entire Category object back into memory just to display its name. Secondly we are pulling all the products of the Food category into memory just to work out how many there are, not only does this result in more memory usage and more data being transferred from the database but we are also issuing a SELECT * FROM dbo.Categories when we really only need to query for the count!

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace CreateNavigationQuerySample
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var ctx = new ProductCatalog())
            {
                var prod = ctx.Products.First();
                Console.WriteLine("{0} belongs to the {1} category.", prod.Name, prod.Category.Name);

                var food = ctx.Categories.Find("FOOD");
                Console.WriteLine("There are {0} food products.", food.Products.Count());
            }

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }

    public class ProductCatalog : DbContext
    {
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
    }

    public class Category
    {
        public string CategoryId { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Product> Products { get; set; }
    }

    public class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public virtual Category Category { get; set; }
    }
}

The Solution

What we really want is a way to get a query that represents the contents of a navigation property for a given entity, this is possible by making use of the underlying ObjectContext from our ProductCatalog. We are going to make use of the RelationshipManager from the ObjectContext to achieve the mission at hand. The API for doing this is pretty undiscoverable and makes use of a lot of strings so lets wrap it up in a nice method and replace those ugly strings with a nice strongly typed lambda. You’ll notice I have defined two overloads to deal with reference and collection navigations. Here is our updated context:

public class ProductCatalog : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    public IQueryable<TResult> CreateNavigationQuery<TSource, TResult>(TSource entity, Expression<Func<TSource, TResult>> navigationPath)
    {
        return CreateNavigationQuery<TResult>(entity, (MemberExpression)navigationPath.Body);
    }

    public IQueryable<TResult> CreateNavigationQuery<TSource, TResult>(TSource entity, Expression<Func<TSource, ICollection<TResult>>> navigationPath)
    {
        return CreateNavigationQuery<TResult>(entity, (MemberExpression)navigationPath.Body);
    }

    private IQueryable<TResult> CreateNavigationQuery<TResult>(object entity, MemberExpression navigationPath)
    {
        var navigationName = navigationPath.Member.Name;

        var ose = this.ObjectContext.ObjectStateManager.GetObjectStateEntry(entity);
        var rm = this.ObjectContext.ObjectStateManager.GetRelationshipManager(entity);

        var entityType = (EntityType)ose.EntitySet.ElementType;
        var navigation = entityType.NavigationProperties[navigationName];

        var relatedEnd = rm.GetRelatedEnd(navigation.RelationshipType.FullName, navigation.ToEndMember.Name);

        return ((dynamic)relatedEnd).CreateSourceQuery();
    }
}

The Solution in Action

Once we have our CreateNavigationQuery method defined we can re-write our Main method to avoid querying and loading all that extra data:

static void Main(string[] args)
{
    using (var ctx = new ProductCatalog())
    {
        var prod = ctx.Products.First();
        var categoryName = ctx.CreateNavigationQuery(prod, p => p.Category).Select(c => c.Name).Single();
        Console.WriteLine("{0} belongs to the {1} category.", prod.Name, categoryName);

        var food = ctx.Categories.Find("FOOD");
        var prodCount = ctx.CreateNavigationQuery(food, c => c.Products).Count();
        Console.WriteLine("There are {0} food products.", prodCount);
    }

    Console.WriteLine("Press any key to exit.");
    Console.ReadKey();
}

VB.Net & Overloads

If you are a C# user then the compiler will take care of working out which overload of CreateNavigationQuery to use. If you are using VB.Net then you’ll need to help the compiler out when you call the collection based overload by explicitly including the types for the generics. Here is the sample usage in VB.Net:

Sub Main()
    Using ctx = New ProductCatalog()
        Dim prod = ctx.Products.First()
        Dim categoryName = ctx.CreateNavigationQuery(prod, Function(p) p.Category).[Select](Function(c) c.Name).[Single]()
        Console.WriteLine("{0} belongs to the {1} category.", prod.Name, categoryName)

        Dim food = ctx.Categories.Find("FOOD")
        Dim prodCount = ctx.CreateNavigationQuery(Of Category, Product)(food, Function(c) c.Products).Count()
        Console.WriteLine("There are {0} food products.", prodCount)
    End Using

    Console.WriteLine("Press any key to exit.")
    Console.ReadKey()
End Sub

Summary & Source Code

Using the RelationshipManager from the underlying ObjectContext we were able to add a helper method to our context that creates a query for the contents of a navigation property. This reduces the need to bring redundant data into memory, ultimately resulting in better performance for our application.

You can download the completed source code in C# and VB.Net here, I make no claims to being fluent in VB.Net so complaints about syntax and conventions will be cheerfully accepted ignored 🙂