简体   繁体   English

OData $ expand,DTO和Entity Framework

[英]OData $expand, DTOs, and Entity Framework

I have a basic WebApi service setup with a database first EF DataModel set up. 我有一个基本的WebApi服务设置,数据库首先设置EF DataModel。 I am running the nightly builds of WebApi, EF6, and the WebApi OData packages. 我正在运行WebApi,EF6和WebApi OData包的每晚构建。 (WebApi: 5.1.0-alpha1, EF: 6.1.0-alpha1, WebApi OData: 5.1.0-alpha1) (WebApi:5.1.0-alpha1,EF:6.1.0-alpha1,WebApi OData:5.1.0-alpha1)

The database has two tables: Product and Supplier. 该数据库有两个表:产品和供应商。 A Product can have one Supplier. 产品可以有一个供应商。 A Supplier can have multiple Products. 供应商可以拥有多种产品。

I have also created two DTO 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; }
}

I have set up my WebApiConfig as follows: 我已按如下方式设置了我的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());
}

I have set up my two controllers as follows: 我按如下方式设置了两个控制器:

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

Here is the metadata that gets returned. 这是返回的元数据。 As you can see, the navigation properties are set up correctly: 如您所见,导航属性设置正确:

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

So the normal array of odata queries work fine: /odata/product?$filter=Name+eq+'Product1' and /odata/supplier?$select=Id for example all work fine. 所以odata查询的正常数组工作正常:/ odata / product?$ filter = Name + eq +'Product1'和/ odata / supplier?$ select = Id例如所有工作正常。

The problem is when I attempt to work with $expand. 问题是当我尝试使用$ expand时。 If I were to do /odata/supplier?$expand=Products, I of course get an error: 如果我要做/ odata / supplier?$ expand = Products,我当然会收到错误:

"The specified type member 'Products' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported." “LINQ to Entities不支持指定的类型成员'Products'。仅支持初始化程序,实体成员和实体导航属性。”

Update: I keep getting the same questions so I am adding more information. 更新:我一直在收到相同的问题,所以我要添加更多信息。 Yes, the navigation properties are set up correctly as can be seen in the metadata information I posted above. 是的,导航属性设置正确,如我在上面发布的元数据信息中所示。

This is not related to methods being missing on the controller. 这与控制器上缺少的方法无关。 If I were to create a class that implements IODataRoutingConvention, /odata/supplier(1)/product would be parsed out as "~/entityset/key/navigation" just fine. 如果我要创建一个实现IODataRoutingConvention的类,/ odata / supplier(1)/ product将被解析为“〜/ entityset / key / navigation”就好了。

If I were to bypass my DTOs completely and just return the EF generated classes, $expand works out of the box. 如果我完全绕过我的DTO并返回EF生成的类,$ expand开箱即用。

Update 2: If I change my Product class to the following: 更新2:如果我将Product类更改为以下内容:

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

    public string Name { get; set; }

    public virtual Supplier Supplier { get; set; }
}

and then change the ProductController to this: 然后将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 
                } 
            });
    }
}

If I were to call /odata/product I would get back what I expected. 如果我打电话给/ odata /产品,我会回到我的预期。 An array of Products with the Supplier field not returned in the response. 在响应中未返回“供应商”字段的产品数组。 The sql query generated joins and selects from the Suppliers table, which would make sense to me if not for the next query results. sql查询生成了来自Suppliers表的连接和选择,如果不是下一个查询结果,这对我来说是有意义的。

If I were to call /odata/product?$select=Id, I would get back what I would expect. 如果我打电话给/ odata / product?$ select = Id,我会回复我的期望。 But $select translates to a sql query that does not join to the suppliers table. 但$ select转换为不加入供应商表的SQL查询。

/odata/product?$expand=Product fails with a different error: / odata / product?$ expand =产品失败并出现不同的错误:

"The argument to DbIsNullExpression must refer to a primitive, enumeration or reference type." “DbIsNullExpression的参数必须引用原语,枚举或引用类型。”

If I change my Product Controller to the following: 如果我将产品控制器更改为以下内容:

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, and /odata/product?$expand=Supplier return the correct results, but obviously the .ToList() defeats the purpose a bit. / odata / product,/ odata / product?$ select = Id,和/ odata / product?$ expand =供应商返回正确的结果,但显然.ToList()有点失败了。

I can try to modify the Product Controller to only call .ToList() when an $expand query is passed, like so: 我可以尝试修改Product Controller,只在传递$ expand查询时调用.ToList(),如下所示:

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

Unfortunately, when I call /odata/product?$select=Id or /odata/product?$expand=Supplier it throws a serialization error because returnValue can't be cast to IQueryable. 不幸的是,当我调用/ odata / product?$ select = Id或/ odata / product?$ expand = Supplier时会抛出序列化错误,因为returnValue无法转换为IQueryable。 I can be cast though if I call /odata/product. 如果我打电话给/ odata / product,我可以演员。

What is the work around here? 这里的工作是什么? Do I just have to skip trying to use my own DTOs or can/should I roll my own implementation of $expand and $select? 我只是不得不跳过尝试使用我自己的DTO,或者我应该/我应该推出自己的$ expand和$ select实现吗?

The underlying issue was fixed in EF 6.1.0. 基本问题已在EF 6.1.0中修复。 See https://entityframework.codeplex.com/workitem/826 . 请参阅https://entityframework.codeplex.com/workitem/826

You have not set up the entity relationships in your web-api. 您尚未在web-api中设置实体关系。 You need to add more methods to your controllers. 您需要向控制器添加更多方法。

I assume the following url's don't work as well: /odata/product(1)/Supplier This is because the relationship isn't set. 我假设以下网址不起作用: /odata/product(1)/Supplier这是因为没有设置关系。

Add the following method to your controller and I think it should solve the issue: 将以下方法添加到您的控制器,我认为它应该解决问题:

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

I think that matched your naming. 我认为这符合您的命名。 Fix them as needed. 根据需要修复它们。 Look at http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations for more info. 有关详细信息, 参阅http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations Your model structure is very similar. 您的模型结构非常相似。

You should use an ICollection navigation property instead of an IQueryable . 您应该使用ICollection导航属性而不是IQueryable These types are very different. 这些类型非常不同。 Not sure that's your problem, but worth fixing. 不确定这是你的问题,但值得修复。

$ expand命令仅在控制器操作将MaxExpansionDepth参数添加到Queryable属性(大于0)时才有效。

[Queryable(MaxExpansionDepth = 1)]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM