Подтвердить что ты не робот

OData $expand, DTO и Entity Framework

У меня есть базовая настройка сервиса WebApi с базой данных, сначала созданной EF DataModel. Я запускаю ночные сборки WebApi, EF6 и WebApi OData. (WebApi: 5.1.0-alpha1, EF: 6.1.0-alpha1, WebApi OData: 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="StackOverflowExample.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="StackOverflowExample.Models.StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartner" ToRole="Products" FromRole="ProductsPartner" />
   </EntityType>
   <Association Name="StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartner">
    <End Type="StackOverflowExample.Models.Product" Role="Products" Multiplicity="*" />
    <End Type="StackOverflowExample.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="StackOverflowExample.Models.Product" />
    <EntitySet Name="supplier" EntityType="StackOverflowExample.Models.Supplier" />
     <AssociationSet Name="StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartnerSet" Association="StackOverflowExample.Models.StackOverflowExample_Models_Supplier_Products_StackOverflowExample_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/поставщик? $Expand = Продукты, я, конечно, получаю сообщение об ошибке:

"Указанный член типа 'Products' не поддерживается в LINQ to Entities. Поддерживаются только инициализаторы, сущности и свойства навигации сущности.

Update: Я продолжаю получать одни и те же вопросы, поэтому добавляю больше информации. Да, свойства навигации настроены правильно, как можно видеть в информации метаданных, опубликованной выше.

Это не связано с отсутствием методов на контроллере. Если бы я должен был создать класс, который реализует IODataRoutingConvention,/odata/supplier (1)/product будет разбираться как "~/entityset/key/navigation" просто отлично.

Если бы я полностью обходил мои DTO и просто возвращал созданные EF классы, $expand работает из коробки.

Обновление 2: Если я изменю класс Product на следующее:

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 = Поставщик, он выдает ошибку сериализации, потому что returnValue не может быть передан в IQueryable. Меня можно отбросить, если я вызываю /odata/product.

Какая работа здесь? Должен ли я просто пропустить попытку использовать свои собственные DTO, или я могу/должен ли я использовать собственную реализацию $expand и $select?

4b9b3361

Ответ 2

Вы не настроили отношения сущности в своем веб-api. Вам нужно добавить дополнительные методы к вашим контроллерам.

Я предполагаю, что следующий 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 для получения дополнительной информации. Структура вашей модели очень похожа.

Ответ 3

Вы должны использовать свойство навигации ICollection вместо IQueryable. Эти типы очень разные. Не уверен, что ваша проблема, но стоит исправить.

Ответ 4

Команда $expand работает только в том случае, если действие контроллера имеет аргумент MaxExpansionDepth, добавленный к атрибуту Queryable, который больше 0.

[Queryable(MaxExpansionDepth = 1)]