Pergunta sobre .net, linq-to-entities, entity-framework – Posso projetar uma referência opcional de uma entidade em uma referência opcional do tipo de resultado da projeção?

13

Diga, eu tenho duas entidades:

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; }
}

Customer é umopcional (anulável) referência emOrder (talvez o sistema suporte pedidos "anônimos").

Agora, quero projetar algumas propriedades de um pedido em um modelo de exibição, incluindo algumas propriedades do clienteE se o pedido tem um cliente. Eu tenho duas classes de modelos de visão, em seguida:

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; }
}

Se oCustomer seria umrequeridos propriedade de navegação emOrder Eu poderia usar a seguinte projeção e funciona porque eu posso ter certeza que umCustomer sempre existe para qualquerOrder:

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();

Mas isso não funciona quandoCustomer é opcional e o pedido com o IDsomeOrderId não tem cliente:

A EF reclama que o valor materializado parao.Customer.SalesLevel éNULL e não pode ser armazenado nointPropriedade não anulávelCustomerViewModel.SalesLevel. Isso não é surpreendente e o problema poderia ser resolvido fazendoCustomerViewModel.SalesLevel do tipoint? (ou geralmentetodos propriedades anuláveis)

Mas eu realmente preferiria issoOrderViewModel.CustomerViewModel é materializado comonull quando o pedido não tem cliente.

Para conseguir isso, tentei o seguinte:

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();

Mas isso lança a infame exceção LINQ to Entities:

Não é possível criar um valor constante do tipo 'CustomerViewModel'. Apenas tipos primitivos (por exemplo, 'Int32', 'String' e 'Guid' ') são suportados neste contexto.

eu acho que: null é o "valor constante" paraCustomerViewModel o que não é permitido.

Desde a atribuiçãonull não parece ser permitido eu tentei introduzir uma propriedade de marcador emCustomerViewModel:

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

E então a projeção:

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();

Isso também não funciona e lança a exceção:

O tipo 'CustomerViewModel' aparece em duas inicializações incompatíveis estruturalmente dentro de uma única consulta LINQ to Entities. Um tipo pode ser inicializado em dois locais na mesma consulta, mas apenas se as mesmas propriedades estiverem definidas nos dois locais e essas propriedades forem definidas na mesma ordem.

A exceção é clara o suficiente para corrigir o problema:

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();

Isso funciona, mas não é uma solução muito boa para preencher todas as propriedades com valores fictícios ounull explicitamente.

Questões:

É o último código trecho a única solução, além de fazer todas as propriedades doCustomerViewModel anulável?

Simplesmente não é possível materializar uma referência opcional paranull em uma projeção?

Você tem uma ideia alternativa de como lidar com essa situação?

(Eu estou apenas definindo o geralEstrutura de entidade tag para esta pergunta porque eu acho que esse comportamento não é específico da versão, mas não tenho certeza. Eu testei os trechos de código acima com EF 4.2 /DbContext/ Código-primeiro. Editar: mais duas tags adicionadas.

@QuintonBernhardt: Não, não encontrei uma solução. Eu trabalho nisso principalmente tornando todas as propriedades do modelo de exibição aninhada anuláveis. Slauma
Você já encontrou uma solução para isso? e se assim você se importaria de compartilhar :) Quinton Bernhardt

Sua resposta

1   a resposta
3

ble do DbQuery. Se você está procurando uma solução alternativa, por que não fazer a projeção depois que os dados foram recuperados do banco de dados e não é mais uma 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();

A desvantagem é que você está buscando todas as colunas Order e Customer do banco de dados. Você pode limitar isso selecionando apenas as colunas que você precisa do Order em um tipo anônimo e ...

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();
Eu estou ciente de ambas as soluções alternativas. O primeiro é terrível, para ser honesto. Eu teria que usarInclude para o cliente, além de carregar todas as propriedades do pedido (ou carregamento lento que causaria várias consultas) e, em seguida, lançar a maior parte das propriedades carregadas na memória quando eu projetar. O segundo é OK, apenas mais código para escrever, mas não exatamente o que eu estava procurando. No entanto, +1 para o exemplo na parte 2 :) BTW: eu normalmente usoAsEnumerable() ao invés deToList() para alternar de LINQ para entidades para LINQ para objetos. Tem um pouco menos sobrecarga. Slauma
Obrigado, bom saber. Quinton Bernhardt

Perguntas relacionadas