Welcome to MSDN Blogs Sign in | Join | Help
Tip 37 – How to do a Conditional Include

Problem

Someone asked how to do a Conditional Include a couple of days ago on StackOverflow.

They wanted to query for some entity (lets say Movies) and eager load some related items (lets say Reviews) but only if the reviews match some criteria (i.e. Review.Stars == 5).

Unfortunately though this isn’t strictly supported by EF’s eager loading, i.e. ObjectQuery<Movie>.Include(…) because Include(..) is all or nothing.

Solution

But there is a workaround.

Here is an example scenario to make this ‘real’:

public class Movie
{
    public int ID {get;set;}
    public string Name {get;set;}
    public string Genre {get;set;}
    public List<Review> Reviews {get;set;}
}

public class Review
{
    public int ID {get;set;}
    public int Stars {get;set;} 

    public string Summary {get;set;}
    public Movie Movie {get;set;}
    public User User {get;set;}
}

Imagine if you want to retrieve all ‘Horror’ movies and all of their 5 star reviews.

You can do so like this:

var dbquery =
   from movie in ctx.Movies
   where movie.Genre == “Horror”
   select new {
      movie, 
      reviews = from review in movie.Reviews
                where review.Stars == 5
                select review
   };

var movies = dbquery
   .AsEnumerable()
   .Select(m => m.movie);

Now why does this work?

Well the first query creates a new instance of an anonymous type containing each Horror movie and just it’s 5 star ratings.

The second query runs in memory using LINQ to Objects thanks to the AsEnumerable() call, and simply ‘unwraps’ the movie from its anonymous type wrapper.

And interestingly each movie will also have just it’s five star reviews loaded!

So this code:

foreach(var movie in movies)
{
    foreach(var review in movie.Reviews)
        Assert(review.Rating == 5);
}

Will pass with no Asserts.

This works because EF implements something called relationship fix-up.

Relationship fix-up ensures that related objects are automatically linked when the second entity enters the ObjectContext.

And because we are loading both the Movie and a filtered list of it’s Reviews, both enter the ObjectContext, and EF makes sure they are automatically linked, which means the matching Reviews are present in the appropriate Movie.Reviews collection.

i.e. Conditional Include.

There are number of different twists on this theme:

  • Issue two separate queries: one for the Movies, one for the Reviews, and let relationship fix-up do the rest.
  • Issue a select many type query as shown here.
  • Sorting relationship - see Tip1

Once you understand how relationship fix-up works you can really use it to your advantage.

Enjoy.

Posted: Tuesday, October 13, 2009 6:51 AM by AlexJ

Comments

AlexY said:

Thanks Alex!

The only change I would do is

var movies = dbquery

  .AsEnumerable().Where(m => m.Reviews.Count() >1)

  .Select(m => m.movie);

to avoid empty entries.

# October 14, 2009 1:27 PM

Jiri Cincura said:

Hi Alex,

it's similar approach I used in http://blog.cincura.net/229660-load-with-filtering-or-limiting/ .

# October 19, 2009 1:04 AM

Gabriel Susai said:

Alex,

I have done the same with Model First driven but I am getting a wrong count on the orders.

Through profiler, I get two queries a filtered one and one for only orders in Console.Writeline.

Please help. Below is the code

BlackDiamondContainer bdc = new BlackDiamondContainer();

           var dbquery =

                  from c in bdc.Customers

                  select new

                  {

                       customer = c,

                      ListofOrders = from order in c.Orders

                               where order.Total == 120

                               select order

                  };

           var Customers = dbquery

              .AsEnumerable()

              .Select(m => m.customer);

           foreach (Customer cust in Customers)

           {

               Console.WriteLine(cust.Orders.Count);

           }

# November 3, 2009 2:15 PM

markgmarkg said:

Hi,

You said: "Issue two separate queries: one for the Movies, one for the Reviews"

Does it mean that Reviews should "include" Movie objects as well to make it work?

Thank you

# December 14, 2009 5:53 AM

AlexJ said:

@Mark,

No when you issue two queries, there should be no need for any includes, because the EF automatically links related entities in memory. So if you issue one query for a movie, and one of the reviews for that movie that also meet some criteria, then after both queries, movie.Reviews should be partially loaded - with just the reviews you want.

Alex

# December 14, 2009 8:26 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

  
Enter Code Here: Required

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Page view tracker