是否可以将实体的可选引用投影到投影结果类型的可选引用中?



比如说,我有两个实体:

public class Customer
{
    public int Id { get; set; }
    public int SalesLevel { get; set; }
    public string Name { get; set; }
    public string City { get; set; }
}
public class Order
{
    public int Id { get; set; }
    public DateTime DueDate { get; set; }
    public string ShippingRemark { get; set; }
    public int? CustomerId { get; set; }
    public Customer Customer { get; set; }
}

CustomerOrder 中的可选(可为空)引用(可能系统支持"匿名"订单)。

现在,我想将订单的某些属性

投影到视图模型中,包括客户的某些属性(如果订单有客户)。然后我有两个视图模型类:

public class CustomerViewModel
{
    public int SalesLevel { get; set; }
    public string Name { get; set; }
}
public class OrderViewModel
{
    public string ShippingRemark { get; set; }
    public CustomerViewModel CustomerViewModel { get; set; }
}

如果CustomerOrder必需的导航属性,我可以使用以下投影并且它有效,因为我可以确定任何Order始终存在Customer

OrderViewModel viewModel = context.Orders
    .Where(o => o.Id == someOrderId)
    .Select(o => new OrderViewModel
    {
        ShippingRemark = o.ShippingRemark,
        CustomerViewModel = new CustomerViewModel
        {
            SalesLevel = o.Customer.SalesLevel,
            Name = o.Customer.Name
        }
    })
    .SingleOrDefault();

但是,当Customer是可选的并且 ID 为 someOrderId 的订单没有客户时,这不起作用:

  • EF 抱怨 o.Customer.SalesLevel 的具体化值NULL并且不能存储在int中,而不是可空属性CustomerViewModel.SalesLevel中。这并不奇怪,这个问题可以通过使int?类型(或通常所有属性都为空)的CustomerViewModel.SalesLevel来解决。

  • 但我实际上更希望OrderViewModel.CustomerViewModel在订单没有客户时实现null

为此,我尝试了以下方法:

OrderViewModel viewModel = context.Orders
    .Where(o => o.Id == someOrderId)
    .Select(o => new OrderViewModel
    {
        ShippingRemark = o.ShippingRemark,
        CustomerViewModel = (o.Customer != null)
            ? new CustomerViewModel
              {
                  SalesLevel = o.Customer.SalesLevel,
                  Name = o.Customer.Name
              }
            : null
    })
    .SingleOrDefault();

但这会引发臭名昭著的 LINQ to Entities 异常:

无法创建类型为"客户视图模型"的常量值。只 基元类型(例如"Int32"、"字符串"和"Guid")是 在此上下文中受支持。

我想: null是不允许CustomerViewModel的"常量值"。

由于似乎不允许分配null,我尝试在CustomerViewModel中引入标记属性:

public class CustomerViewModel
{
    public bool IsNull { get; set; }
    //...
}

然后投影:

OrderViewModel viewModel = context.Orders
    .Where(o => o.Id == someOrderId)
    .Select(o => new OrderViewModel
    {
        ShippingRemark = o.ShippingRemark,
        CustomerViewModel = (o.Customer != null)
            ? new CustomerViewModel
              {
                  IsNull = false,
                  SalesLevel = o.Customer.SalesLevel,
                  Name = o.Customer.Name
              }
            : new CustomerViewModel
              {
                  IsNull = true
              }
    })
    .SingleOrDefault();

这也不起作用并引发异常:

类型"客户视图模型"出现在两个结构上不兼容的 单个 LINQ to 实体查询中的初始化。类型可以是 在同一查询中的两个位置初始化,但前提是相同 属性在两个位置都设置,这些属性在 相同的顺序。

异常很清楚如何解决问题:

OrderViewModel viewModel = context.Orders
    .Where(o => o.Id == someOrderId)
    .Select(o => new OrderViewModel
    {
        ShippingRemark = o.ShippingRemark,
        CustomerViewModel = (o.Customer != null)
            ? new CustomerViewModel
              {
                  IsNull = false,
                  SalesLevel = o.Customer.SalesLevel,
                  Name = o.Customer.Name
              }
            : new CustomerViewModel
              {
                  IsNull = true,
                  SalesLevel = 0, // Dummy value
                  Name = null
              }
    })
    .SingleOrDefault();

这有效,但用虚拟值填充所有属性或显式null不是一个很好的解决方法。

问题:

  1. 除了使CustomerViewModel的所有属性可为空之外,最后一个代码片段是唯一的解决方法吗?

  2. 是否根本不可能在投影中实现对null的可选引用?

  3. 您有如何处理这种情况的替代想法吗?

(我只为这个问题设置了通用实体框架标签,因为我想这种行为不是特定于版本的,但我不确定。我已经使用 EF 4.2/DbContext/Code-First 测试了上面的代码片段。编辑:又添加了两个标签。

我也无法让投影在 DbQuery 的 IQueryable 实现上工作。 如果您正在寻找解决方法,那么为什么不在从 Db 检索数据并且它不再是 E.F. DbQuery 之后进行投影......

OrderViewModel viewModel = context.Orders
    .Where(o => o.Id == someOrderId)
     // get from db first - no more DbQuery
    .ToList()
    .Select(o => new OrderViewModel
    {
        ShippingRemark = o.ShippingRemark,
        CustomerViewModel = o.Customer == null ? null : new CustomerViewModel
        {
            SalesLevel = o.Customer.SalesLevel,
            Name = o.Customer.Name
        }
    })
    .SingleOrDefault();

缺点是您从数据库中获取所有订单和客户列。 您可以通过仅从"顺序"中选择所需的列到匿名类型,然后

...
OrderViewModel viewModel = context.Orders
    .Where(o => o.Id == someOrderId)
    .Select(o => new { ShippingRemark = o.ShippingRemark, Customer = o.Customer })
     // get from db first - no more DbQuery
    .ToList()
    .Select(o => new OrderViewModel
    {
        ShippingRemark = o.ShippingRemark,
        CustomerViewModel = o.Customer == null ? null : new CustomerViewModel
        {
            SalesLevel = o.Customer.SalesLevel,
            Name = o.Customer.Name
        }
    })
    .SingleOrDefault();

最新更新