EF Core 2子实体.ThenInclude用法及EF6泛型Get方法适配问题
首先,咱们先理清EF Core 2里ThenInclude的基本用法——它用来在Include加载了一级导航属性后,继续加载该属性的子实体关联。举个实际例子:假设你有Order实体,包含Customer(单个实体)和OrderItems(集合),而OrderItem又关联Product,要一次性加载所有关联数据的话,写法是这样的:
var order = context.Orders .Include(o => o.Customer) // 加载一级关联Customer .Include(o => o.OrderItems) // 加载一级关联OrderItems集合 .ThenInclude(oi => oi.Product) // 基于OrderItems加载子关联Product .FirstOrDefault(o => o.Id == orderId);
接下来看你原来的EF6泛型Get方法,EF6里Include有直接接收多个表达式的重载,但EF Core 2没有这个重载,而且要支持ThenInclude的多级关联,咱们得调整方法的设计,这里有两种实用方案:
方案一:适配多一级Include(仅支持单层关联)
如果你只需要加载一级导航属性,不需要多级子关联,可以把原来的方法改成循环添加每个Include,因为EF Core的Include每次只能接收一个表达式:
public abstract class DataContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>, IDataContext { public DataContext(DbContextOptions options) : base(options) { } public T Get<T>(int id, params Expression<Func<T, object>>[] includes) where T : class, IEntity { var query = this.Set<T>().AsQueryable(); // 循环添加每个一级Include foreach (var includeExpr in includes) { query = query.Include(includeExpr); } return query.FirstOrDefault(t => t.Id == id); } }
调用方式和原来差不多,比如加载Order的Customer和OrderItems:
var order = dataContext.Get<Order>(1, o => o.Customer, o => o.OrderItems);
但这个方案不支持多级子关联(比如没法直接加载OrderItems里的Product),所以更推荐下面的方案。
方案二:支持任意层级的Include/ThenInclude(灵活度最高)
通过添加一个Func<IQueryable<T>, IQueryable<T>>类型的参数,让调用者可以自己构建完整的关联加载链,这样不管是一级还是多级子关联都能处理:
public abstract class DataContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>, IDataContext { public DataContext(DbContextOptions options) : base(options) { } public T Get<T>(int id, Func<IQueryable<T>, IQueryable<T>> includeConfig = null) where T : class, IEntity { var query = this.Set<T>().AsQueryable(); // 如果传入了关联配置,就应用到查询上 if (includeConfig != null) { query = includeConfig(query); } return query.FirstOrDefault(t => t.Id == id); } }
调用的时候就可以自由组合Include和ThenInclude了,比如加载Order的Customer、OrderItems及其关联的Product:
var order = dataContext.Get<Order>(1, query => query .Include(o => o.Customer) .Include(o => o.OrderItems) .ThenInclude(oi => oi.Product));
这个方案的优势在于完全适配EF Core的关联加载语法,不管你需要多少层级的子实体,都能灵活配置,而且写法和直接写EF Core查询保持一致,学习成本很低。
额外提示
如果你的IEntity接口定义了Id属性(比如int Id { get; set; }),那FirstOrDefault(t => t.Id == id)的写法是没问题的;如果Id的类型可能变化(比如有的实体用Guid),还可以把方法改成泛型Id的版本:public T Get<T, TId>(TId id, ...) where T : class, IEntity<TId>,这样通用性更强。
内容的提问来源于stack exchange,提问作者Matthew Flynn

