EntityFramework 核心项目为 Skip/Take 分页加入行

EntityFramework Core Project Joined Rows For Skip/Take Pagination

使用 Asp.Net 3.1 Core EntityFramework Core LINQ,假设我有一个订单 table 和一个客户 table:

public class Order
{
     public long Id { get; set; }
     public string CustomerId { get; set; }
     public int Total {get; set;}
     public virtual Customer Customer{ get; set; }
}

public class Customer : ApplicationUser
{
   public long Id {get; set;}
   public virtual ICollection<Order> Orders { get; set; }
}

最终,我想要 return 宇宙中每个客户的列表,即使他们没有订单(左外?),但我还希望每个订单都有一行。所以像:

Customer    Order    Total
--------    -----    -----
1           null     null
2           100      5
2           101      199
3           null     null
4           200      299
4           201      399

我 运行 遇到的困难是我需要在服务器上执行此操作,因为我需要使用 skip/take 对这些数据进行分页。直接 Context.Customer.Include(x => x.Order) 不会按照我需要的方式投影行,以便使用 skip/take 进行分页,我被语法困住了。

在直接 LINQ 中这可能吗?如果是这样,LINQ 会是什么样子?

提前致谢!

这是可能的,但是根据我的测试,它似乎无法在 EF Core 3.1 中运行。

通常你可以这样做:

var results = context.Customers
    .SelectMany(c => c.Orders.DefaultIfEmpty()
        .Select(o => new { c.CustomerId, o.OrderId, o.Total }) 
    );

然后从那里使用分页 /w .Skip.Take.

以上内容适用于 EF6,但对于 EF Core 3.1 (3.1.15) c.CustomerId 由于某种原因 return 编辑为 #null。似乎内部 Select EF Core 无法/不会解析回外部 SelectMany 客户引用。我看到了一些关于 EF Core 中 DefaultIfEmpty 实现问题的参考。似乎是核心团队一直忽视的另一个功能。

我已经发布了这个,以防有人知道该行为的解释或解决方法,或者可以验证 EF Core 5 是否仍然如此。

我可以使用 EF Core 3.1 得到的东西更难看。它需要 return 值的定义类型,您可能已经拥有或没有:

[Serializable]
public class OrderData 
{
    public int CustomerId { get; set; }
    public int? OrderId { get; set; }
    public int? Total { get; set; }
}

var results = context.Customers
    .Where(c => !c.Orders.Any())
    .Select(c => new OrderData { CustomerId = c.CustomerId, OrderId = null, Total = null })
    .Union(context.Customers
        .SelectMany(c => c.Orders.Select(o => new OrderData
        {
            CustomerId = c.CustomerId,
            OrderId = o.OrderId,
            Total = o.Total
        })));

有趣的是 return 对象类型是必需的,因为它不会联合匿名类型(预期),尽管它似乎确实要求您显式初始化每个空 属性 或你得到一个投影异常。

从那里您可以对结果进行排序(合并后)并应用分页。

你想要的是左外连接。 Microsoft 在 https://docs.microsoft.com/en-us/dotnet/csharp/linq/perform-left-outer-joins.

上提供了有关在 LINQ 中执行左外部联接的信息

适应您的代码如下所示:

from customer in context.Customers
  join order in context.Orders on customer equals order.Customer into gj
  from suborder in gj.DefaultIfEmpty()
  select new { CustomerId = customer.Id, OrderId = suborder?.Id, Total = suborder?.Total };

从那里您可以应用分页。

如果您更喜欢使用 lambda 和扩展方法,请参阅 How do you perform a left outer join using linq extension methods

您使用 LINQ 查询语法查找的查询类似于这样

var query =
    from c in context.Customers
    from o in c.Orders.DefaultIfEmpty()
    select new
    {
        CustomerId = c.Id,
        OrderId = (long?)o.Id,
        Total = (int?)o.Total
    };

一些注意事项。

首先,DefaultIfEmpty() 是产生左外连接的原因。没有它,它将被视为内部连接。

其次,由于现在 Order 数据来自左外连接的(可选)右侧,您需要考虑它可能是 null。在 LINQ to Object 中,需要使用带 null 检查的条件运算符或 null 合并运算符。在 LINQ to Entities 中,这由 SQL 自然处理,但您需要将不可空字段的结果类型更改为其等效的可为空字段。在匿名投影中,这是通过如上所示的显式转换实现的。

最后,为什么要查询语法?当然,它可以用方法语法编写(SelectMany,如 Steve Py 的回答),但由于 EF Core 团队似乎正在针对编译器生成的 LINQ 结构进行测试,如果你使用“错误的”,你很容易遇到 EF Core 错误“超载/模式。这里的“错误”并不是真正的错误,只是 EF Core 翻译器没有考虑到的东西。这里的“正确”是将 SelectMany 重载与结果选择器一起使用:

var query = context.Customers
    .SelectMany(c => c.Orders.DefaultIfEmpty(), (c, o) => new 
    {
        CustomerId = c.Id,
        OrderId = (long?)o.Id,
        Total = (int?)o.Total
    });

使用查询语法就不会出现此类问题。