EF CTP4 Tips & Tricks: Querying Navigations Without Loading

Posted on August 14, 2010. Filed under: Entity Framework | Tags: , , , , , , , , |

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 :)

About these ads

Make a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

6 Responses to “EF CTP4 Tips & Tricks: Querying Navigations Without Loading”

RSS Feed for RoMiller.com Comments RSS Feed

Rowan – Very cool stuff. Is there currently a way to create a one-to-one navigational property like I described in this post on stack overflow?

Hi,
Yes you can map a 1:1, assuming you have the following classes:
public class Product
{
public int ProductId { get; set; }
public ProductDetail Detail { get; set; }
}

public class ProductDetail
{
[Key]
public int ProductId { get; set; }
public virtual Product Product { get; set; }
}

The mapping would be as follows:
modelBuilder.Entity()
.HasRequired(p => p.Detail)
.WithRequiredPrincipal(d => d.Product)
.HasConstraint((d, p) => d.ProductId == p.ProductId);

~Rowan

thanks a lot, I’ve been looking for the strongly-typed way to do include, cause strings are not very friendly

Rowan,

This is the same thing that this blog post? http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

Example:

context.Entry(princess)
.Collection(p => p.Unicorns)
.Query()
.Where(u => u.Name.StartsWith(“B”))
.Load();

Hello Rowan. I’ve trying to use this with EF v 5.0, but without any success
First, I was forced to cast (this) to IObjectContextAdapter, in order to gain access to .ObjectContext
Second, this line of code:

var ose = ((IObjectContextAdapter) this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity);
throws an exception of type
System.InvalidOperationException: The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type ‘System.Data.Entity.DynamicProxies…

What I’ve done wrong?

Hi Row!

Maybe you can help me understand the reason behind the design choice of not returning an IQueryable from Collection Navigation Properties…

SO Thread here: http://goo.gl/k8ISgr

Thanks


Where's The Comment Form?

    About

    Rowan works as a Program Manager for the ADO.NET Entity Framework team at Microsoft. He speaks at technical conferences and blogs at romiller.com. Rowan lives in Seattle, Washington with his wife Athalie. Prior to moving to the US he resided in the small state of Tasmania in Australia. Outside of technology Rowan's passions include snowboarding, mountain biking, horse riding, rock climbing and pretty much anything else that involves being active. The primary focus of his life, however, is to follow Jesus.

    RSS

    Subscribe Via RSS

    • Subscribe with Bloglines
    • Add your feed to Newsburst from CNET News.com
    • Subscribe in Google Reader
    • Add to My Yahoo!
    • Subscribe in NewsGator Online
    • The latest comments to all posts in RSS

    Meta

Liked it here?
Why not try sites on the blogroll...

Follow

Get every new post delivered to your Inbox.

Join 146 other followers

%d bloggers like this: