Return 父项和子项在单个 entity framework 查询中不返回 IQueryable 或 IEnumerable?

Return parent and children in single entity framework query WITHOUT returning IQueryable or IEnumerable?

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

我想知道如何在不将子集合定义为 IQueryableIEnumerable 的情况下编写一个 linq 查询来在一次访问数据库的过程中获取父项及其子项。

例如,假设从服务中 returned 的类型是 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 查询写入 select 到匿名类型,然后将其映射到 return 的实际类型。我们使用可以协助映射的AutoMapper。这行不通的任何原因?我是否必须在映射对象之前调用 .ToList() 以确保我在映射时不会 运行 遇到同样的问题?

如果您的 Parent 实体中有这样的导航 属性:

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

那我建议你在你的服务层新建两个类(可以是ParentViewModel,和ChildViewModel)来保存你的查询结果。在这两个 类 中只声明您在表示层中需要的属性。然后使用 Automapper.

将您的实体与您的 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 的扩展方法。

正如您在我上面引用的 link 中看到的那样,Automapper 支持嵌套映射,因此如果您在 ParentViewModel 中有一个 ICollection<ChildViewModel> 类型的 属性 并且您还将 Child 实体与其 ViewModel 映射,然后 Automapper 将自动尝试从一种类型映射到另一种类型。

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

您可以在内部使用 Anon 查询使用普通 ol' EF 而不使用 AutoMapper 来执行此操作,但仍然会在最终结果中返回强类型 objects:

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 及其所有 children 的 parent ]数组。

关于 SelectMany 的重要说明:您可能想在这里使用 Select/GroupBy

如果您使用 SelectMany,而您的条件 select 只有一个 parent:您将获得 parent 及其所有 children单数组。 (这是我的 use-case。)

如果您使用 SelectMany,并且您的条件 select 多个 parent:您将在一个文件中获得 parent 及其所有 children单个数组。

如果以上和 children (grandparents/grandchildren) 的 parent 满足条件:您将在一个数组中获得完整的家谱,如果你不添加 .Distinct().

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