OData $expand, DTO и Entity Framework
У меня есть базовая настройка сервиса WebApi с базой данных, сначала настроенной EF DataModel. Я запускаю ночные сборки пакетов WebApi, EF6 и ODA WebApi. (WebApi: 5.1.0-alpha1, EF: 6.1.0-alpha1, ODATA WebApi: 5.1.0-alpha1)
База данных имеет две таблицы: продукт и поставщик. Продукт может иметь одного поставщика. Поставщик может иметь несколько продуктов.
Я также создал два класса DTO:
public class Supplier
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual IQueryable<Product> Products { get; set; }
}
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
Я настроил свой WebApiConfig следующим образом:
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder oDataModelBuilder = new ODataConventionModelBuilder();
oDataModelBuilder.EntitySet<Product>("product");
oDataModelBuilder.EntitySet<Supplier>("supplier");
config.Routes.MapODataRoute(routeName: "oData",
routePrefix: "odata",
model: oDataModelBuilder.GetEdmModel());
}
Я настроил два моих контроллера следующим образом:
public class ProductController : ODataController
{
[HttpGet]
[Queryable]
public IQueryable<Product> Get()
{
var context = new ExampleContext();
var results = context.EF_Products
.Select(x => new Product() { Id = x.ProductId, Name = x.ProductName});
return results as IQueryable<Product>;
}
}
public class SupplierController : ODataController
{
[HttpGet]
[Queryable]
public IQueryable<Supplier> Get()
{
var context = new ExampleContext();
var results = context.EF_Suppliers
.Select(x => new Supplier() { Id = x.SupplierId, Name = x.SupplierName });
return results as IQueryable<Supplier>;
}
}
Вот метаданные, которые возвращаются. Как видите, свойства навигации настроены правильно:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="StackruExample.Models" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
<EntityType Name="Supplier">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<NavigationProperty Name="Products" Relationship="StackruExample.Models.StackruExample_Models_Supplier_Products_StackruExample_Models_Product_ProductsPartner" ToRole="Products" FromRole="ProductsPartner" />
</EntityType>
<Association Name="StackruExample_Models_Supplier_Products_StackruExample_Models_Product_ProductsPartner">
<End Type="StackruExample.Models.Product" Role="Products" Multiplicity="*" />
<End Type="StackruExample.Models.Supplier" Role="ProductsPartner" Multiplicity="0..1" />
</Association>
</Schema>
<Schema Namespace="Default" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="product" EntityType="StackruExample.Models.Product" />
<EntitySet Name="supplier" EntityType="StackruExample.Models.Supplier" />
<AssociationSet Name="StackruExample_Models_Supplier_Products_StackruExample_Models_Product_ProductsPartnerSet" Association="StackruExample.Models.StackruExample_Models_Supplier_Products_StackruExample_Models_Product_ProductsPartner">
<End Role="ProductsPartner" EntitySet="supplier" />
<End Role="Products" EntitySet="product" />
</AssociationSet>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Таким образом, обычный массив запросов odata работает нормально: /odata/product?$ Filter=Name+eq+'Product1' и /odata/supplier?$ Select=Id, например, все работают нормально.
Проблема в том, когда я пытаюсь работать с $expand. Если я буду делать /odata/supplier?$ Expand=Products, я, конечно, получаю ошибку:
"Элемент указанного типа" Продукты "не поддерживается в LINQ to Entities. Поддерживаются только инициализаторы, элементы сущностей и свойства навигации сущностей".
Обновление: я продолжаю получать одни и те же вопросы, поэтому я добавляю больше информации. Да, свойства навигации настроены правильно, как видно из информации о метаданных, которую я разместил выше.
Это не связано с отсутствием методов на контроллере. Если бы я должен был создать класс, который реализует IODataRoutingConvention, /odata/supplier(1)/product будет просто разобран как "~/entityset/key/navigation".
Если бы мне пришлось полностью обойти мои DTO и просто вернуть сгенерированные EF классы, $expand работает из коробки.
Обновление 2: если я изменю свой класс продукта на следующий:
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual Supplier Supplier { get; set; }
}
и затем измените ProductController на это:
public class ProductController : ODataController
{
[HttpGet]
[Queryable]
public IQueryable<Product> Get()
{
var context = new ExampleContext();
return context.EF_Products
.Select(x => new Product()
{
Id = x.ProductId,
Name = x.ProductName,
Supplier = new Supplier()
{
Id = x.EF_Supplier.SupplierId,
Name = x.EF_Supplier.SupplierName
}
});
}
}
Если бы я позвонил / odata / product, я бы получил то, что ожидал. Массив Товаров с полем Поставщик, не возвращенный в ответе. Созданный SQL-запрос объединяет и выбирает из таблицы поставщиков, что будет иметь смысл, если бы не результаты следующего запроса.
Если бы я позвонил / odata / product? $ Select=Id, я бы получил то, что ожидал. Но $select переводит в SQL-запрос, который не присоединяется к таблице поставщиков.
/odata/product?$expand= Произошел сбой продукта с другой ошибкой:
"Аргумент DbIsNullExpression должен ссылаться на примитив, перечисление или ссылочный тип".
Если я изменю свой контроллер продукта на следующее:
public class ProductController : ODataController
{
[HttpGet]
[Queryable]
public IQueryable<Product> Get()
{
var context = new ExampleContext();
return context.EF_Products
.Select(x => new Product()
{
Id = x.ProductId,
Name = x.ProductName,
Supplier = new Supplier()
{
Id = x.EF_Supplier.SupplierId,
Name = x.EF_Supplier.SupplierName
}
})
.ToList()
.AsQueryable();
}
}
/ odata / product, / odata / product? $select=Id и /odata/product?$expand= Поставщик возвращает правильные результаты, но, очевидно,.ToList() немного побеждает цель.
Я могу попытаться изменить Контроллер Продукта так, чтобы он вызывал.ToList() только когда передан запрос $ expand, например:
[HttpGet]
public IQueryable<Product> Get(ODataQueryOptions queryOptions)
{
var context = new ExampleContext();
if (queryOptions.SelectExpand == null)
{
var results = context.EF_Products
.Select(x => new Product()
{
Id = x.ProductId,
Name = x.ProductName,
Supplier = new Supplier()
{
Id = x.EF_Supplier.SupplierId,
Name = x.EF_Supplier.SupplierName
}
});
IQueryable returnValue = queryOptions.ApplyTo(results);
return returnValue as IQueryable<Product>;
}
else
{
var results = context.EF_Products
.Select(x => new Product()
{
Id = x.ProductId,
Name = x.ProductName,
Supplier = new Supplier()
{
Id = x.EF_Supplier.SupplierId,
Name = x.EF_Supplier.SupplierName
}
})
.ToList()
.AsQueryable();
IQueryable returnValue = queryOptions.ApplyTo(results);
return returnValue as IQueryable<Product>;
}
}
}
К сожалению, когда я вызываю / odata / product? $ Select=Id или / odata / product? $ Expand=Supplier, он выдает ошибку сериализации, потому что returnValue не может быть приведено к IQueryable. Я могу быть брошен, хотя, если я позвоню / odata / product.
Какая работа здесь? Должен ли я просто пропустить попытки использовать свои собственные DTO или я могу / должен свернуть мою собственную реализацию $ expand и $select?
4 ответа
Основная проблема была исправлена в EF 6.1.0. См. https://entityframework.codeplex.com/workitem/826.
Вы должны использовать ICollection
свойство навигации вместо IQueryable
, Эти типы очень разные. Не уверен, что это ваша проблема, но стоит исправить.
Команда $expand работает только в том случае, если к действию контроллера добавлен аргумент MaxExpansionDepth, добавленный к атрибуту Queryable, который больше 0.
[Queryable(MaxExpansionDepth = 1)]
Вы не настроили отношения сущностей в вашем веб-интерфейсе. Вам нужно добавить больше методов для ваших контроллеров.
Я предполагаю, что следующие URL не работают также: /odata/product(1)/Supplier
Это потому, что отношения не установлены.
Добавьте следующий метод к вашему контроллеру, и я думаю, что это должно решить проблему:
// GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
var context = new ExampleContext();
Product product = context.EF_Products.FirstOrDefault(p => p.ID == key);
if (product == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return product.Supplier;
}
Я думаю, что это соответствует вашему имени. Исправьте их по мере необходимости. Посмотрите на http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations для получения дополнительной информации. Структура вашей модели очень похожа.