在单个实体框架查询中返回父项和子项,而不返回 IQueryable 或 IEnumerable

本文关键字:返回 IQueryable IEnumerable 查询 框架 单个 实体 | 更新日期: 2023-09-27 18:30:42

我们有一条规则,不公开服务层之外的IQueryable<T>IEnumerable<T>,因此下游代码无法修改进入数据库的查询。这意味着我们返回的类型类似于 IList 或 ICollection。

我想知道如何编写一个 linq 查询来获取父级及其子级,而无需将子集合定义为 IQueryableIEnumerable

例如,假设从服务返回的类型是ICollection<Parent>其中Parent定义为:

public class Parent
{
    public int ParentId { get; set; }
    public string ParentName { get; set; }
    public ICollection<Child> Children { get; set; }
}

如果我将查询定义为这样...

from p in dbContext.Parents
where p.Name.Contains("test")
select new Parent
{
    Children =from c in dbContext.Children
    where c.ParentId == p.ParentId
    select c;
}

它不编译,因为IQueryable没有实现ICollection。如果我将.ToList()添加到父集合和子集合中,它会编译,但现在它将为每个父集合单独访问数据库以获取其子集合。

当我写这个问题时,我突然想到,也许答案是编写 Linq 查询以选择匿名类型,然后将其映射到要返回的实际类型。我们使用自动映射器,它可以帮助映射。有什么理由这行不通吗?在映射对象之前,我是否必须调用.ToList()以确保在映射时不会遇到相同的问题?

在单个实体框架查询中返回父项和子项,而不返回 IQueryable 或 IEnumerable

如果Parent实体中有如下所示的导航属性:

public class Parent
{
    public int ParentId { get; set; }
    public string ParentName { get; set; }
    public virtual ICollection<Child> Children { get; set; }
   //..
}

然后,我建议您在服务层中创建两个新类(可以是ParentViewModelChildViewModel)来保存查询结果。在这两个类中,仅声明表示层中所需的属性。然后使用自动映射器将实体与 ViewModel 类映射。

之后,您可以执行如下查询:

var query =dbContext.Parents
                    .Include(p=>p.Children) // load the related entities as part of the query
                    .Where(p=>p.ParentName.Contains("test"))
                    .ProjectTo<ParentViewModel>();

使用自动映射程序ProjectTo扩展方法。

正如您在上面引用的链接中看到的那样,Automapper 支持嵌套映射,因此,如果您在ParentViewModel具有类型 ICollection<ChildViewModel> 的属性,并且还使用其 ViewModel 映射了Child实体,那么 Automapper 将帮助自动尝试从一种类型映射到另一种类型。

所有这些都将在一次往返数据库的过程中发生,因为ProjectTo是一种IQueryable<TEntity>扩展方法,并且它被转换为Select

您可以使用普通的 EF 和不使用内部使用 Anon 查询的自动映射器来执行此操作,但仍会在最终结果中返回强类型对象:

public class Human
{
    public int Id { get; set; }
    public ICollection<Human> Children { get; set; }
    public Human Parent { get; set; }
    [ForeignKey("Parent")]
    public int ParentId { get; set; }
}
var family = await db.Humans
    .Where(h => h.SomeCriteriaForParent == criteria)
    .Select(h => new {
        H = h,
        HH = h.Children
    })
    .SelectMany(x => x.HH.Concat(new[] { x.H }))
    .ToArrayAsync();

例如,如果您的条件是 Id,而您的 ID 是 5,那么您将在单个Human[]数组中获得 ID 为 5 的父级及其所有子级。

关于SelectMany的重要说明:您可能希望在此处使用Select/GroupBy

如果使用 SelectMany ,并且条件仅选择一个父项:则将在单个数组中获取父级及其所有子项。(这是我的用例。

如果使用 SelectMany ,并且条件选择多个父项:则将父项及其所有子项放在一个数组中。

如果上述和父母是孩子(祖父母/孙子孙女)符合标准:您将在单个数组中获得完整的家谱,如果您不添加.Distinct(),您可能会受到欺骗。

如果您使用 Select : 您将获得由父级分隔的子级集,分成多个数组。这不是我的用例,但我怀疑如果它是你的,你在这里真正想要的是一个 GroupBy。