90

As the title suggest I am looking for a way to do a where clause in combination with an include.

Here is my situations: I am responsible for the support of a large application full of code smells. Changing too much code causes bugs everywhere so I am looking for the safest solution.

Let's say I have an object Bus and an object People(Bus has a navigation prop Collection of People). In my Query I need to select all the Busses with only the Passengers that are awake. This is a simplistic dummy example

In the current code:

var busses = Context.Busses.Where(b=>b.IsDriving == true);
foreach(var bus in busses)
{
   var passengers = Context.People.Where(p=>p.BusId == bus.Id && p.Awake == true);
   foreach(var person in passengers)
   {
       bus.Passengers.Add(person);
   }
}

After this code the Context is disposed and in the calling method the resulting Bus entities are Mapped to a DTO class (100% copy of Entity).

This code causes multiple calls to DB which is a No-Go, so I found this solution ON MSDN Blogs

This worked great when debugging the result but when the entities are mapped to the DTO (Using AutoMapper) I get an exception that the Context/Connection has been closed and that the object can't be loaded. (Context is always closed can’t change this :( )

So I need to make sure that the Selected Passengers are already loaded (IsLoaded on navigation property is also False). If I inspect the Passengers collection The Count also throws the Exception but there is also a collection on the Collection of Passegers called “wrapped related entities” which contain my filtered objects.

Is there a way to load these wrapped related entities into the whole collection? (I can't change the automapper mapping config because this is used in the whole application).

Is there another way to Get the Active Passengers?

Any hint is welcome...

Edit

Answer of Gert Arnold doesn't work because the data isn't loaded eagerly. But when I simplify it and delete the where it is loaded. This is realy strange since the execute sql returns all the passengers in both cases. So there must be a problem when putting the results back into the entity.

Context.Configuration.LazyLoadingEnabled = false;
var buses = Context.Busses.Where(b => b.IsDriving)
        .Select(b => new 
                     { 
                         b,
                         Passengers = b.Passengers
                     })
        .ToList()
        .Select(x => x.b)
        .ToList();

Edit2

After a lot of struggle the answer of Gert Arnold work! As Gert Arnold suggested you need to disable Lazy Loading and Keep it OFF. This will ask for some extra changes to the appliaction since the prev developer loved Lazy Loading -_-

Beejee
  • 1,836
  • 2
  • 17
  • 31
  • this is just an example written in stackoveflow without intellisense :p Is fixed now – Beejee May 28 '13 at 18:37
  • Can you also show us what a sample of the relevant parts of the class implementation would look like for Bus, People, and Passengers (Such as foreign keys and Navigation properties)? – Travis J May 28 '13 at 18:49
  • Passengers is a navigation prop yeah – Beejee May 28 '13 at 20:04
  • 2
    I'm somewhat surprised this question has almost no attention given it considering how hard it was for me to find and how it's a great way to limit the amount of data EF queries the database for. Have people not seen the queries that EF creates for the database to run? – Ellesedil Apr 30 '14 at 21:20
  • @Ellesedil Your sentiment is right, but those "long" EF queries are only long to a human. They are actually very efficient. You'd be hard-pressed to write a query with a faster execution plan than the ones EF generates on a regular basis. – Suamere Aug 31 '18 at 19:57
  • How does b.Passengers end up with the correct values? If you did this with non EF this would not work. You never set b.Passengers, just Passengers on the anonymous type – Dave Amour Sep 11 '22 at 06:55

5 Answers5

96

This feature has now been added to Entity Framework core 5. For earlier versions you need a work-around (note that EF6 is an earlier version).

Entity Framework 6 work-around

In EF6, a work-around is to first query the required objects in a projection (new) and let relationship fixup do its job.

You can query the required objects by

Context.Configuration.LazyLoadingEnabled = false;
// Or: Context.Configuration.ProxyCreationEnabled = false;
var buses = Context.Busses.Where(b => b.IsDriving)
            .Select(b => new 
                         { 
                             b,
                             Passengers = b.Passengers
                                           .Where(p => p.Awake)
                         })
            .AsEnumerable()
            .Select(x => x.b)
            .ToList();

What happens here is that you first fetch the driving buses and awake passengers from the database. Then, AsEnumerable() switches from LINQ to Entities to LINQ to objects, which means that the buses and passengers will be materialized and then processed in memory. This is important because without it EF will only materialize the final projection, Select(x => x.b), not the passengers.

Now EF has this feature relationship fixup that takes care of setting all associations between objects that are materialized in the context. This means that for each Bus now only its awake passengers are loaded.

When you get the collection of buses by ToList you have the buses with the passengers you want and you can map them with AutoMapper.

This only works when lazy loading is disabled. Otherwise EF will lazy load all passengers for each bus when the passengers are accessed during the conversion to DTOs.

There are two ways to disable lazy loading. Disabling LazyLoadingEnabled will re-activate lazy loading when it is enabled again. Disabling ProxyCreationEnabled will create entities that aren't capable of lazy loading themselves, so they won't start lazy loading after ProxyCreationEnabled is enabled again. This may be the best choice when the context lives longer than just this single query.

But... many-to-many

As said, this work-around relies on relationship fixup. However, as explained here by Slauma, relationship fixup doesn't work with many-to-many associations. If Bus-Passenger is many-to-many, the only thing you can do is fix it yourself:

Context.Configuration.LazyLoadingEnabled = false;
// Or: Context.Configuration.ProxyCreationEnabled = false;
var bTemp = Context.Busses.Where(b => b.IsDriving)
            .Select(b => new 
                         { 
                             b,
                             Passengers = b.Passengers
                                           .Where(p => p.Awake)
                         })
            .ToList();
foreach(x in bTemp)
{
    x.b.Pasengers = x.Passengers;
}
var busses = bTemp.Select(x => x.b).ToList();

...and the whole thing becomes even less appealing.

Third-party tools

There is a library, EntityFramework.DynamicFilters that makes this a lot easier. It allows you to define global filters for entities, that will subsequently be applied any time the entity is queried. In your case this could look like:

modelBuilder.Filter("Awake", (Person p) => p.Awake, true);

Now if you do...

Context.Busses.Where(b => b.IsDriving)
       .Include(b => b.People)

...you'll see that the filter is applied to the included collection.

You can also enable/disable filters, so you have control over when they are applied. I think this is a very neat library.

There is a similar library from the maker of AutoMapper: EntityFramework.Filters

Entity Framework core work-around

Since version 2.0.0, EF-core has global query filters. These can be used to set predefined filter on entities that are to be included. Of course that doesn't offer the same flexibility as filtering Include on the fly. Although global query filters are a great feature, so far the limitation is that a filter can't contain references to navigation properties, only to the root entity of a query. Hopefully in later version these filters will attain wider usage.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • 1
    Per my now missing comment about EntityFrameworkDynamicFilters unfortunately not being an option for DB First: https://github.com/zzzprojects/EntityFramework.DynamicFilters/issues/12 – xr280xr Nov 17 '17 at 00:03
  • Thank you @Gert Arnold for your detailed elaboration! This fixed my issue and made me learn new things again ;) ! – Dimitri Jan 04 '20 at 20:00
  • 1
    @Gert Arnold, how can I Include a navigation property into that query? Let's say that Passengers has a Entity Address as property, how would I include that into this query? I don't have the .Include option in the Select and before the Select, I can but that won't work ... – Dimitri Jan 04 '20 at 20:06
  • 1
    @Dimitri By a nested projection: `Passengers = b.Passengers.Where(p => p.Awake).Select(p => new { p, p.Addresses })`. It's not elegant, not at all. – Gert Arnold Jan 04 '20 at 20:11
  • @GertArnold, of course ... Thank you for pointing that out! What would be the most performant way, looping through the passengers, adding them to the Busses (like your many to many example or as suggested above? I don't mind the elegance but I choose performance above all in this case. – Dimitri Jan 04 '20 at 20:17
  • 1
    @Dimitri The crucial part is that one SQL query be performed. Anything that requires n+1 queries quickly deteriorates performance. It probably doesn't matter much what kind of in-memory post-processing is carried out. – Gert Arnold Jan 04 '20 at 20:28
43

Now EF Core 5.0's Filter Include method now supports filtering of the entities included

var busses = _Context.Busses
                .Include(b => b.Passengers
                                       .Where(p => p.Awake))
            .Where(b => b.IsDriving);
Satpal
  • 132,252
  • 13
  • 159
  • 168
  • 4
    I extracted the sql query with "ToQueryString()" and it wasn't including the ".Where(p => p.Awake)". is it really working? – rami bin tahin Nov 24 '21 at 14:25
29

Disclaimer: I'm the owner of the project Entity Framework Plus

EF+ Query IncludeFilter feature allows filtering related entities.

var buses = Context.Busses
                   .Where(b => b.IsDriving)
                   .IncludeFilter(x => x.Passengers.Where(p => p.Awake))
                   .ToList();

Wiki: EF+ Query IncludeFilter

Jonathan Magnan
  • 10,874
  • 2
  • 38
  • 60
  • This works great. How do you turn off the IncludeFilter, though? – jDave1984 Sep 28 '18 at 20:10
  • You currently cannot turn it off. – Jonathan Magnan Sep 29 '18 at 01:24
  • It works, but it more slow than Include with AsNotTracking and unnecessary data. I have not many data (48 rows vs 1 row with where clause). But when i use IncludeFilter i can't mixed it with Include and AsNoTracking (your library limitations). But i need to select also 6 additional objects. In small test it takes 6 seconds vs 3 seconds. – Константин Золин Mar 13 '20 at 02:03
  • Try `IncludeOptimized`, maybe you will have more chance with this one. You can still use `IncludeFilter` or `IncludeOptimized` even without a filter if you need additional objects – Jonathan Magnan Mar 13 '20 at 12:25
  • The filter is working (it compiles and run at least) but the include isn't, in this example and my case, Passengers will be an empty collection – Flou Jul 21 '22 at 14:52
2

In my case the Include was an ICollection, and also did not want to return them, I just needed to get the main entities but filtered by the referenced entity. (in other words, Included entity), what I ended up doing is this. This will return list of Initiatives but filtered by InitiativeYears

return await _context.Initiatives
                .Where(x => x.InitiativeYears
                    .Any(y => y.Year == 2020 && y.InitiativeId == x.Id))
                .ToListAsync();

Here the Initiatives and the InitiativeYears has following relationship.

public class Initiative
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<InitiativeYear> InitiativeYears { get; set; }
}

public class InitiativeYear
{
    public int Year { get; set; }
    public int InitiativeId { get; set; }
    public Initiative Initiative { get; set; }
}
Nishan
  • 3,644
  • 1
  • 32
  • 41
-2

For any one still curious about this. there builtin functionality for doing this in EF Core. using .Any inside of a where clause so the code would like similar to something like this

_ctx.Parent
    .Include(t => t.Children)
    .Where(t => t.Children.Any(t => /* Expression here */))
ezhupa99
  • 1,838
  • 13
  • 18
Bjarke Handsdal
  • 219
  • 1
  • 6
  • 7
    I tested this. Does not work as you intend. All it will do is exclude parent that does not have Any children matching the expression. – Skystrider Mar 05 '20 at 22:43