views:

431

answers:

3

Hi folks,

I have two linq (to EF4) queries, which return different results. The first query contains the correct results, but is not formatted/projected right.

the second query is what i want but it missing some data.

Schema

alt text

Query 1

var xxxx = (from cp in _connectedClientRepository
            .GetConnectedClients(new[] { "LogEntry", "LogEntry.GameFile" })
            .AsExpandable()
            .Where(predicate)
            select cp)
    .ToList();

alt text

Notice the property GameFile . It is not null. This is great :) Notice the linq query? I'm eager loading a LogEntry and then eager loading a GameFile (for each eager loaded LogEntry).

This is what i'm after -> for each LogEntry that is eager loaded, please eager load the GameFile. But this projection result is wrong...

Ok.. next...

Query 2

var yyy = (from cp in _connectedClientRepository
            .GetConnectedClients(new[] { "LogEntry", "LogEntry.GameFile" })
            .AsExpandable()
            .Where(predicate)
        select cp.LogEntry)
    .ToList();

alt text

NOTE: the image above has a typo in it ... please note the include associations typed code is correct (ie. LogEntry.GameFile) while the image has it typo'd.

Correct projection now -> all LogEntries results. But notice how the GameFile property is now null? I'm not sure why :( I thought i correctly eager loaded the correct chain. So this is the correct projection but with incorrect results.

Obligatory Repository code.

public IQueryable<ConnectedClient> GetConnectedClients(
    string[] includeAssociations)
{
    return Context.ConnectedClients
        .IncludeAssociations(includeAssociations)
        .AsQueryable();
}

public static class Extensions
{
    public static IQueryable<T> IncludeAssociation<T>(
        this IQueryable<T> source, string includeAssociation)
    {
        if (!string.IsNullOrEmpty(includeAssociation))
        {
            var objectQuery = source as ObjectQuery<T>;

            if (objectQuery != null)
            {
                return objectQuery.Include(includeAssociation);
            }
        }

        return source;
    }

    public static IQueryable<T> IncludeAssociations<T>(
        this IQueryable<T> source, params string[] includeAssociations)
    {
        if (includeAssociations != null)
        {
            foreach (string association in includeAssociations)
            {
                source = source.IncludeAssociation(association);
            }
        }

        return source;
    }
}

Updates

  • 1 : Fixed some typo's in noticed in the code samples.
  • 2 : Added repository code to help anyone who is confused.
A: 

It seems you need one more repository method, that will do this for you; _connectedClientsRepository.GetLogEntriesOfConnectedClients().

Tomas Lycken
hm. I think i need to respectfully disagree with you here. My repository method returns an IQueriable and the rest of the linq query does the filter, instead of hardcoding the filter in a specific repository method.
Pure.Krome
+2  A: 

I suspect Craig Stuntz' suggestion may work, but if it doesn't, the following should certainly work:

 var xxxx =_connectedClientRepository
        .GetConnectedClients(new[] { "LogEntry", "LogEntry.GameFile" })
        .AsExpandable()
        .Where(predicate)
        .ToList() // execute query
        .Select(cp => cp.LogEntry); // use linq-to-objects to project the result
jeroenh
yep. that's exactly what i'm doing now, as Craig suggested.
Pure.Krome
Oh and i think u need a ToList() after the Select....
Pure.Krome
@Pure.Krome: not necessarily. The first ToLists() ensures the EF query is executed, and puts the results in a List<ConnectedClient>. The Select() projection transforms this into an IEnumerable<LogEntry> without actually iterating over the list yet. If you need to iterate just once, there's no need to add a second ToList(). But unless it's a huge list, we're talking about micro-optimization here...
jeroenh
You could also use AsEnumerable() instead of ToList() to force the projection into LINQ to Objects. That is, if you didn't want to execute the EF query just yet.
Lucas
@jeroenh : ah, ok. When i tried it without the ToList() i got a compiler error becuase i was trying to return that .. as an IList<..> . gotcha.
Pure.Krome
+1  A: 

Include() works on the query results, rather than the intermediate queries. You can read more about Include() in this post. So one solution is to apply the Include() to the whole query, like this:

var q = ((from cp in _connectedClientRepository.GetConnectedClients()
                                               .AsExpandable()
                                               .Where(predicate) 
          select cp.LogEntry) 
         as ObjectQuery).Include("GameFile").ToList();

That will probably work, but it's ugly. Can we do better?

I can think of two ways around this issue. Mostly, it depends upon whether or not you actually need entity types returned. I can't say whether this is the case without seeing the rest of your code. Generally, you need to return entity types when you are going to update (or otherwise modify) them. If you are selecting for display or calculation purposes, it's often a better strategy to return POCOs instead of entity types. You can do this with projection, and of course it works in EF 1. In this case, you would change your repository method to return POCO types:

 public IQueryable<ClientInfo> GetConnectedClients()
 {
      return from cp in _context.Clients
             where // ...
             select new ClientInfo
             {
                 Id = cp.Id,
                 ClientName = cp.ClientName,
                 LogEntry = new LogEntryInfo
                            {
                                LogEntryId = cp.LogEntry.LogEntryId,
                                GameFile = new GameFileInfo
                                           {
                                               GameFileId = cp.LogEntry.GameFile.GameFileId,
                                               // etc.
                                           },
                                // etc.
                            },
                  // etc.
             };
 }

Note that when you use projection there is no eager loading, no lazy loading, and no explicit loading. There is only your intention, expressed as a query. The LINQ provider will figure out what you need, even if you further compose the query outside the repository!

On the other hand, you might need to return entity types instead of POCOs, because you intend to update them. In that case, I would write a separate repository method for the LogEntries, as Tomas suggested. But I would only do this if I intended to update them, and I might write it as an update method, rather than a "Get" method.

Craig Stuntz
@Craig Stuntz : thanks heaps for the detailed reply.The method you described above is similar to what i sorta used to do with Linq-To-Sql also.. but it looks/ feels like it's way to cumbersome. Especially now that EF4 handles POCO's already, if they are mapped. So in your first example above, that will be a POCO, not an entity. I also _never_ use the entities in any of my code .. bleh. For updates, etc. i always load the latest, left-to-right update, then save. But yeah, this post has been the most helpful. The key here was that the include needs to happen AFTER the shaping. :)
Pure.Krome