Problem with Eager Loading Nested Navigation Based on Abstract Entity
Asked Answered
B

1

5

I a portion of my EF model that looks like this:

enter image description here

Summary:

  • Location has many Posts
  • Post is an abstract class
  • Discussion derives from Post
  • Discussions have many Comments

Now, the query i'm trying to achieve:

Get information about Location Id 1234, including any Discussions and Comments associated with those Discussions.

I can get discussions and the comments like this:

var discussions = ctx.Posts
                     .OfType<Discussion>()
                     .Include(x => x.Comments)
                     .ToList();

But i can't seem to get it based on the Posts navigation on the Location entity.

I've tried this:

var locationWithDiscussionsAndComments = ctx
                    .Locations
                    .Include(x => x.Posts
                                   .OfType<Discussion>()
                                   .Select(y => y.Comments))
                    .SingleOrDefault();

Which compiles, but i get the error:

System.ArgumentException: The include path expression must refer to a property defined by the entity, optionally also with nested properties or calls to Select. Parameter name: path

Any ideas? I could probably go "backwards" from the Posts:

var locationWithDiscussionsAndComments = ctx
                   .Posts
                   .Include(x => x.Location)
                   .OfType<Discussion>()
                   .Include(x => x.Comments)
                   .Where(x => x.LocationId == 1234)
                   .Select(x => x.Location)
                   .ToList();

But that is both hairy and semantically wrong in terms of my repositories (i shouldn't have to go through a post repository to get information about a location).

Any ideas?

EDIT

So after having a bigger think about it, i realized that OfType<T> is a filter operation. As as we know, EF does not support filtering with eager loading. The only options are retrieving everything, or using anonymous type projection.

No way i can retrieve everything, as there is far too much meta data involved. So i'm attempting the anonymous type projection.

Brinkmanship answered 4/3, 2011 at 3:19 Comment(5)
Yes, after all the Lambda expression in the new Include overload is merely a property selector and you can't have any sort of filtering logic in it. Like you mentioned, your best bet is to use anonymous projections here. Are you using DbContext here?Trescott
@Morteza - yes, im using DbContext, behind a Repository<T>, where T is an aggregate root, which Location and Post both are. So seperate repositories.Brinkmanship
Ok, then you can use the new Query method to apply filters when explicitly loading related entities (it's not eager loading though) as explained here: blogs.msdn.com/b/adonet/archive/2011/01/31/…Trescott
@Morteza - i've heard mentions of that, but i'm using POCO's, so ctx.Posts is ICollection<Post>, not DbSet<T>. How would i do it with POCO's?Brinkmanship
Are you sure? I think it should be ObjectSet<Post>?Trescott
T
6

The new Query method might help you:

var location = context.Locations.SingleOrDefault();

context.Entry(location)
       .Collection(l => l.Posts)
       .Query()
       .OfType<Discussion>()
       .Load();


Repository Implementation:

We can add a new LoadProperty generic method to the Repository<T> class that leverages this new QUery method:

public void LoadProperty<TElement>(T entity, 
        Expression<Func<T, ICollection<TElement>>> navigationProperty,
        Expression<Func<TElement, bool>> predicate) where TElement : class
{
    _context.Set<T>().Attach(entity);

    _context.Entry(entity)         
            .Collection(navigationProperty)
            .Query()
            .Where(predicate)
            .Load();
}

Using the LoadProperty method:

Location location = _locationRepository.Find(1);
_locationRepository.LoadProperty(location, l => l.Posts, p => p is Discussion);
Trescott answered 4/3, 2011 at 16:14 Comment(7)
Morteza - but i'm behind a repository, so in order to get access to the entries, i do repository.Find(), which returns IQueryable<T>, imlpemented under the hood by EF as DbSet<T>. Will i need to add a specialized method to my generic repository interface to do this?Brinkmanship
Yes, you would need do define a new method for that. Please see my updated answer.Trescott
@Morteza - hold on, is this one round trip or two? You've got locationRepository.Find(1) returning a Location, then the LoadProperty call. In this another call? The Find method on my Repositories returns an IQueryable<T> (so IQueryable<Location>). Im hoping to do this will one db trip, otherwise if i'm going to do two trips, i may as well use another specialised call for the second trip (get discussions for a location method).Brinkmanship
Of course it's two trips. If you are looking for 1 then anonymous projection is the way to go. The idea of my LoadProperty method is to have a generic method on the Base Repository class that lazy load a navigation property (based on some criteria) of an object that is already retrieved. BTW, why (in general) your Find method returns IQueryable? How do you execute it then?Trescott
@Morteza - that's a long discussion. :) Essentially im a fan of the "service layer" middle man, where the Controller's (MVC app) call a "service", which executes queries against an IQueryable<T> repository. This way, my Repository stays ultra simple. Okay, so it's two calls. No problems, i'll accept this answer, but in reality i'll stick with using my other specialized method for the second call. The query is too complicated for an anonymous projection query. Thanks buddy.Brinkmanship
No problem dude. I wish I had a better answer for you, but looks like that's the best we can do as of CTP5. Anyways, regarding your service layer, I think you probably have another method in your repository like Execute that you pass the IQueryable which you got from the Find method and it gives you a IEnumerable after executing the IQueryable against your repository's UnitOfWork. Am I guessing correct here?Trescott
@Morteza - no, the service later just invokes the enumerator, eg my "FindById" method looks like this: return repository.Find().WithId(id).SingleOrDefault().Brinkmanship

© 2022 - 2024 — McMap. All rights reserved.