OData $ expand, DTOs e Entity Framework

Eu tenho uma configuração de serviço WebApi básica com um banco de dados primeiro EF DataModel configurado. Eu estou executando as compilações noturnas de WebApi, EF6 e os pacotes OData do WebApi. (WebApi: 5.1.0-alpha1, EF: 6.1.0-alpha1, ODOS do WebApi: 5.1.0-alpha1)

O banco de dados tem duas tabelas: Produto e Fornecedor. Um Produto pode ter um Fornecedor. Um fornecedor pode ter vários produtos.

Eu também criei duas classes 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; }
}

Eu configurei meu WebApiConfig da seguinte maneira:

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

Eu configurei meus dois controladores da seguinte maneira:

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

Aqui estão os metadados que são retornados. Como você pode ver, as propriedades de navegação estão configuradas corretamente:

<?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>

Portanto, o array normal de consultas de odata funciona bem: / odata / product? $ Filter = Nome + eq + 'Product1' e / odata / supplier? $ Select = Id por exemplo, tudo funciona bem.

O problema é quando tento trabalhar com $ expand. Se eu fosse fazer / odata / supplier? $ Expand = Products, é claro que recebo um erro:

"O membro de tipo especificado 'Produtos' não é suportado no LINQ to Entities. Somente inicializadores, membros de entidades e propriedades de navegação de entidade são suportados."

Atualizar: Eu continuo recebendo as mesmas perguntas, então estou adicionando mais informações. Sim, as propriedades de navegação estão configuradas corretamente, como pode ser visto nas informações de metadados que postei acima.

Isso não está relacionado a métodos que estão faltando no controlador. Se eu fosse criar uma classe que implementasse IODataRoutingConvention, / odata / supplier (1) / product seria analisado como "~ / entityset / key / navigation".

Se eu fosse ignorar meus DTOs completamente e apenas retornar as classes geradas pelo EF, o $ expand funcionaria fora da caixa.

Atualização 2: Se eu alterar minha classe de produto para o seguinte:

public class Product
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public virtual Supplier Supplier { get; set; }
}

e então mude o ProductController para isto:

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

Se eu fosse ligar / odata / product eu recuperaria o que eu esperava. Uma matriz de Produtos com o campo Fornecedor não retornada na resposta. A consulta sql gerou junções e seleciona da tabela Fornecedores, o que faria sentido para mim, se não para os próximos resultados da consulta.

Se eu fosse chamar / odata / product? $ Select = Id, eu voltaria o que eu esperaria. Mas $ select se traduz em uma consulta SQL que não se associa à tabela de fornecedores.

/ odata / product? $ expand = Produto falha com um erro diferente:

"O argumento para DbIsNullExpression deve referir-se a um tipo primitivo, de enumeração ou de referência."

Se eu mudar o meu Product Controller para o seguinte:

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 e / odata / product? $ expand = O fornecedor retorna os resultados corretos, mas obviamente o .ToList () anula um pouco o propósito.

Eu posso tentar modificar o Product Controller para chamar apenas .ToList () quando uma consulta $ expand for passada, da seguinte forma:

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

Infelizmente, quando eu chamo / odata / product? $ Select = Id ou / odata / product? $ Expand = O fornecedor lança um erro de serialização porque returnValue não pode ser convertido em IQueryable. Eu posso ser escalado embora se eu chamo / odata / product.

Qual é o trabalho por aqui? Eu só tenho que pular tentando usar meus próprios DTOs ou posso / devo rolar minha própria implementação de $ expand e $ select?

questionAnswers(4)

yourAnswerToTheQuestion