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.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • 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? – Morteza Manavi Mar 04 '11 at 04:29
  • @Morteza - yes, im using `DbContext`, behind a `Repository`, where `T` is an aggregate root, which `Location` and `Post` both are. So seperate repositories. – RPM1984 Mar 04 '11 at 04:59
  • 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: http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx – Morteza Manavi Mar 04 '11 at 05:14
  • @Morteza - i've heard mentions of that, but i'm using POCO's, so `ctx.Posts` is `ICollection`, not `DbSet`. How would i do it with POCO's? – RPM1984 Mar 04 '11 at 06:04
  • Are you sure? I think it should be `ObjectSet`? – Morteza Manavi Mar 04 '11 at 16:06

1 Answers1

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);
Morteza Manavi
  • 33,026
  • 6
  • 100
  • 83
  • Morteza - but i'm behind a repository, so in order to get access to the entries, i do `repository.Find()`, which returns `IQueryable`, imlpemented under the hood by EF as `DbSet`. Will i need to add a specialized method to my generic repository interface to do this? – RPM1984 Mar 04 '11 at 22:41
  • Yes, you would need do define a new method for that. Please see my updated answer. – Morteza Manavi Mar 05 '11 at 22:58
  • @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` (so `IQueryable`). 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). – RPM1984 Mar 07 '11 at 00:29
  • 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? – Morteza Manavi Mar 07 '11 at 02:32
  • @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` 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. – RPM1984 Mar 07 '11 at 02:55
  • 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? – Morteza Manavi Mar 07 '11 at 03:33
  • @Morteza - no, the service later just invokes the enumerator, eg my "FindById" method looks like this: `return repository.Find().WithId(id).SingleOrDefault()`. – RPM1984 Mar 07 '11 at 20:45