Вопрос по .net, entity-framework – Могу ли я проецировать необязательную ссылку на сущность в необязательную ссылку типа результата проекции?

13

Скажем, у меня есть две сущности:

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 являетсяoptional (обнуляемая) ссылка вOrder (возможно, система поддерживает «анонимные» заказы).

Теперь я хочу спроецировать некоторые свойства заказа в модель представления, включая некоторые свойства клиента.if заказ есть у клиента. У меня есть два вида моделей моделей:

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

ЕслиCustomer будетrequired свойство навигации вOrder Я мог бы использовать следующую проекцию, и она работает, потому что я могу быть уверен, чтоCustomer всегда существует для любогоOrder:

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 необязательно и заказ с IdsomeOrderId не имеет клиента:

EF complains that the materialized value for o.Customer.SalesLevel is NULL and cannot be stored in the int, not nullable property CustomerViewModel.SalesLevel. That's not surprising and the problem could be solved by making CustomerViewModel.SalesLevel of type int? (or generally all properties nullable)

But I would actually prefer that OrderViewModel.CustomerViewModel is materialized as null when the order has no customer.

Чтобы добиться этого, я попробовал следующее:

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:

Unable to create a constant value of type 'CustomerViewModel'. Only primitive types (for instance ''Int32', 'String' und 'Guid'') are supported in this context.

я думаю что: 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();

Это тоже не работает и выдает исключение:

The type 'CustomerViewModel' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order.

Исключение достаточно понятно, как решить проблему:

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 в явном виде.

Вопросы:

Is the last code snippet the only workaround, aside from making all properties of the CustomerViewModel nullable?

Is it simply not possible to materialize an optional reference to null in a projection?

Do you have an alternative idea how to deal with this situation?

(Я только устанавливаю общиеentity-framework тег для этого вопроса, потому что я думаю, что это поведение не зависит от версии, но я не уверен. Я тестировал фрагменты кода выше с EF 4.2 /DbContext/ Code-первых. Изменить: добавлены еще два тега.)

Вы когда-нибудь находили решение для этого? и если да, то не могли бы вы поделиться :) Quinton Bernhardt
@QuintonBernhardt: Нет, я не нашел решения. Я обхожу это в основном, обнуляя все свойства модели вложенного представления. Slauma

Ваш Ответ

1   ответ
3

ли вы ищете обходной путь, то почему бы не выполнить проекцию после того, как данные были получены из 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();

Недостатком является то, что вы извлекаете все столбцы Order и Customer из Db. Вы можете ограничить это, выбрав только нужные вам столбцы из Order в анонимный тип, а затем ...

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();
Спасибо, приятно знать.
Мне известны оба обходных пути. Первый ужасный, если честно. Я должен был бы использоватьInclude для клиента в дополнение к загрузке всех свойств заказа (или отложенной загрузке, которая вызовет многократные запросы), а затем выбрасывает большинство загруженных свойств в память, когда я проектирую. Второй в порядке, просто больше кода для написания, но не совсем то, что я искал. Тем не менее +1 для примера в части 2 :) Кстати: я обычно используюAsEnumerable() вместоToList() переключиться с LINQ на Entities на LINQ to Objects. Это немного меньше накладных расходов. Slauma

Похожие вопросы