Могу ли я проецировать необязательную ссылку объекта в необязательную ссылку типа результата проекции?
Скажем, у меня есть два объекта:
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
является необязательной (нулевой) ссылкой в Order
(возможно, система поддерживает "анонимные" заказы).
Теперь я хочу проецировать некоторые свойства заказа в модель представления, включая некоторые свойства клиента , если заказ имеет клиента. У меня есть два класса модели представления:
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
будет обязательным навигационным свойством в Order
, я мог бы использовать следующий проектор, и он работает, потому что я могу быть уверен, что a 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
является необязательным, а порядок с Id someOrderId
не имеет клиента:
-
EF жалуется, что материализованное значение для o.Customer.SalesLevel
равно NULL
и не может быть сохранено в свойстве int
, а не в nullable CustomerViewModel.SalesLevel
. Это неудивительно, и проблема может быть решена путем создания CustomerViewModel.SalesLevel
типа int?
(или вообще всех свойств, нулевых)
-
Но я бы предпочел, чтобы 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:
Невозможно создать постоянное значение типа "CustomerViewModel". Только примитивные типы (например, 'Int32', 'String' und '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();
Это тоже не работает и генерирует исключение:
Тип "CustomerViewModel" отображается в двух структурно несовместимых инициализации в рамках одного запроса LINQ to Entities. Тип может быть инициализируется в двух местах в одном запросе, но только если тот же свойства задаются в обоих местах, и эти свойства задаются в тот же порядок.
Исключением достаточно ясно, как устранить проблему:
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
явно.
Вопросы:
-
Является ли последний фрагмент кода единственным обходным решением, за исключением того, что все свойства CustomerViewModel
могут быть нулевыми?
-
Невозможно ли материализовать необязательную ссылку на NULL
в проекции?
-
Есть ли у вас альтернативная идея, как справиться с этой ситуацией?
(Я устанавливаю общий тег-инфраструктуру для этого вопроса, потому что я предполагаю, что это поведение не зависит от версии, но я не уверен. Я тестировал фрагменты кода выше с помощью EF 4.2/ DbContext
/Code -Первый. Изменить: добавлены еще два тега.)
Ответы
Ответ 1
Я не могу заставить проекцию работать с реализацией IQueryable DbQuery. Если вы ищете обходной путь, то почему бы не сделать проецирование после того, как данные были извлечены из 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();