EF: 用 where 条款包括

正如标题所暗示的那样,我正在寻找一种将 where 子句与 include 结合起来的方法。

我的情况是这样的: 我负责支持一个充满代码味道的大型应用程序。 更改太多的代码会导致到处都是 bug,所以我正在寻找最安全的解决方案。

假设我有一个对象 Bus 和一个对象 People (Bus 有一个导航道具 Collection of People)。 在我的查询中,我需要选择所有的巴士,只有乘客是清醒的。这是一个简单的虚拟示例

在当前代码中:

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);
}
}

在这段代码之后,上下文被释放,并且在调用方法中,生成的总线实体映射到一个 DTO 类(100% 的实体副本)。

这段代码会导致对 DB 的多次调用,这是一个 No-Go,所以我找到了这个解决方案 在 MSDN 博客上

这在调试结果时非常有效,但是当实体映射到 DTO (使用自动映射器)时,我得到一个异常,上下文/连接已经关闭,对象无法加载。(语境总是封闭的,不能改变这一点: ()

因此,我需要确保选定的乘客已经加载(导航属性上的 IsLoadedonisFalse 也是 False)。如果我检查乘客集合 The Count 也会抛出 Exception,但是在乘客集合中还有一个名为“被包装的相关实体”的集合,它包含我过滤的对象。

是否有方法将这些经过包装的相关实体加载到整个集合中? (我不能更改自动映射器映射配置,因为这在整个应用程序中都使用)。

还有别的方法可以获得活跃的乘客吗?

欢迎任何提示。

剪辑

Gert Arnold 的回答不起作用,因为数据没有被急切地加载。 但是当我简化它并删除它加载的地方。这非常奇怪,因为在这两种情况下执行 sql 都会返回所有乘客。因此,当将结果放回到实体中时,一定存在一个问题。

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();

编辑2

经过许多斗争的答案格特阿诺德工作! 正如 Gert Arnold 建议的那样,您需要禁用惰性加载并保持其关闭。 这将要求对应用程序进行一些额外的更改,因为前面的开发人员喜欢 Lazy Loading-_-

102211 次浏览

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.

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

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 */))

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; }
}

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);