EF中导航属性过滤:用Expression还是Predicate?实现疑问
问题描述
我有一个仓储类,支持传入自定义Expression对主实体进行过滤,同时也可传入自定义表达式过滤关联对象。DbContext实体过滤需使用Expression<Func<>>,而一对多导航属性(通常为ICollection<>)在.Include子句中仅支持用Func<>谓词过滤。这让我感到困惑:主实体要求用Expression,导航属性却只能用Func<>。例如以下代码无法编译,问题出在.Include(c => c.Orders.Where(orderCriteria)),因为orderCriteria是一个Expression:
public class Customer { public int Id { get; set; } public virtual ICollection<Order> Orders { get; set;} } public class Order { public int Id { get; set; } public int CustomerId { get; set;} public Customer Customer { get; set; } } public class CustomerRepository { private DbContext _dbContext = new DbContext(); List<Customer> SearchCustomers( Expression<Func<Customer, bool>> customerCriteria, Expression<Func<Order, bool>> orderCriteria) { return _dbContext.Customers.Where(customerCriteria) .Include(c => c.Orders.Where(orderCriteria)).ToList(); // 无法编译 } }
请问我应该调用.Include(c => c.Orders.Where(orderCriteria.Compile()))?还是直接将orderCriteria作为Func<Order,bool>传入?此外,Order实体的查询仍会在数据库层面以SQL执行吗?
解答
1. 两种错误做法的问题
不管是调用orderCriteria.Compile()转换成Func<Order,bool>,还是直接将参数改为Func<Order,bool>,都会导致客户端过滤:
- EF会先从数据库加载所有符合条件的Customer,以及这些Customer关联的全部Order到内存中
- 之后在内存中用
Func谓词筛选Order,完全不会在SQL层面生成Order的过滤条件 - 这种方式性能极差,尤其是当关联Order数量很大时
2. 正确实现:数据库层面过滤Order
要让Order的过滤逻辑在数据库层面执行,需要让EF Core将Expression<Func<Order,bool>>转换成SQL条件。可以通过以下两种方式实现:
方式一:使用投影(推荐,代码简洁)
直接通过Select投影出包含过滤后Order的Customer对象:
List<Customer> SearchCustomers( Expression<Func<Customer, bool>> customerCriteria, Expression<Func<Order, bool>> orderCriteria) { return _dbContext.Customers.Where(customerCriteria) .Select(c => new Customer { Id = c.Id, // 复制Customer的其他必要属性 Orders = c.Orders.Where(orderCriteria).ToList() }).ToList(); }
注意:这种方式返回的Customer实例默认不会被EF上下文跟踪,如果需要跟踪,可以添加.AsTracking()。
方式二:构建表达式树实现带过滤的Include
如果需要保留EF的实体跟踪,且希望用Include语法,可以手动构建表达式树,让EF识别到要调用Queryable.Where而非Enumerable.Where:
List<Customer> SearchCustomers( Expression<Func<Customer, bool>> customerCriteria, Expression<Func<Order, bool>> orderCriteria) { // 构建表达式:c => c.Orders.Where(orderCriteria) var parameter = Expression.Parameter(typeof(Customer), "c"); var ordersProperty = Expression.Property(parameter, nameof(Customer.Orders)); var whereCall = Expression.Call( typeof(Queryable), nameof(Queryable.Where), new[] { typeof(Order) }, ordersProperty, orderCriteria); var includeExpression = Expression.Lambda<Func<Customer, IEnumerable<Order>>>(whereCall, parameter); return _dbContext.Customers.Where(customerCriteria) .Include(includeExpression) .ToList(); }
这种方式会让EF Core生成包含Order过滤条件的SQL,完全在数据库层面完成查询。
3. 版本注意事项
- 上述方案仅适用于EF Core 5.0及以上版本(EF Core 5开始支持Include导航属性的过滤)
- 如果使用EF6,官方不支持Include的过滤逻辑,只能通过投影或加载后客户端过滤实现
内容的提问来源于stack exchange,提问作者zola25

