OData $expand、DTO 和实体框架

Posted

技术标签:

【中文标题】OData $expand、DTO 和实体框架【英文标题】:OData $expand, DTOs, and Entity Framework 【发布时间】:2013-10-19 20:28:07 【问题描述】:

我有一个基本的 WebApi 服务设置,其中首先设置了数据库 EF DataModel。我正在运行 WebApi、EF6 和 WebApi OData 包的夜间构建。 (WebApi:5.1.0-alpha1,EF:6.1.0-alpha1,WebApi OData:5.1.0-alpha1)

数据库有两个表:Product 和 Supplier。一个产品可以有一个供应商。一个供应商可以有多个产品。

我还创建了两个 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="***Example.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="***Example.Models.***Example_Models_Supplier_Products_***Example_Models_Product_ProductsPartner" ToRole="Products" FromRole="ProductsPartner" />
   </EntityType>
   <Association Name="***Example_Models_Supplier_Products_***Example_Models_Product_ProductsPartner">
    <End Type="***Example.Models.Product" Role="Products" Multiplicity="*" />
    <End Type="***Example.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="***Example.Models.Product" />
    <EntitySet Name="supplier" EntityType="***Example.Models.Supplier" />
     <AssociationSet Name="***Example_Models_Supplier_Products_***Example_Models_Product_ProductsPartnerSet" Association="***Example.Models.***Example_Models_Supplier_Products_***Example_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 不支持指定的类型成员‘Products’。仅支持初始化程序、实体成员和实体导航属性。”

更新: 我不断收到相同的问题,因此我正在添加更多信息。是的,从我上面发布的元数据信息中可以看出,导航属性设置正确。

这与控制器上缺少的方法无关。如果我要创建一个实现 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 查询从 Suppliers 表中生成连接和选择,如果不是下一个查询结果,这对我来说是有意义的。

如果我调用 /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=Supplier 返回正确的结果,但显然 .ToList() 有点失败。

我可以尝试将产品控制器修改为仅在传递 $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>;
        
    

不幸的是,当我调用 /odata/product?$select=Id 或 /odata/product?$expand=Supplier 时,它会引发序列化错误,因为 returnValue 无法转换为 IQueryable。如果我调用 /odata/product,我可以被强制转换。

这里有什么工作?我是否只需要跳过尝试使用我自己的 DTO 还是可以/应该推出自己的 $expand 和 $select 实现?

【问题讨论】:

您自己创建了 DTO 类还是使用 EF 的能力从数据库生成模型? 我自己。在模型上生成的那些前面带有“EF_”。例如,ExampleContext 包含一个名为 EF_Products 的 DbSet。当然,这不是我的确切用例。 DTO 与其 EF 创建的对应项匹配的事实只是为了方便示例。 你的 DTO 有相关的导航属性吗? @MikeWasson 是的。请参阅我更新的问题。 @AmITheRWord 是和否。我发现有很多不同的方法可以绕过它。他们都不是完美的。现在在我当前的体系结构中更加复杂,因为我的 ef 实体投射到另一个 dto 中,然后我的服务投射到它自己的 dto 中。最后,我使用的模式并不是一个完整的答案,所以我真的不想在这里发布。如果您想给我发一封电子邮件,我可以为您提供更多信息,请随时访问 outlook dot com 上的 luke.sigler。 【参考方案1】:

基本问题已在 EF 6.1.0 中修复。见https://entityframework.codeplex.com/workitem/826。

【讨论】:

【参考方案2】:

您尚未在 web-api 中设置实体关系。您需要向控制器添加更多方法。

我认为以下网址也不能正常工作:/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 了解更多信息。您的模型结构非常相似。

【讨论】:

这与我的问题不符。 /Products(1)/Supplier 不是我想要完成的。 /Products(1)?$expand=Supplier 是我想要完成的任务。 我认为关键是 /Products(1)/Supplier 可能因为 /Products(1)$expand=Supplier 不工作的原因不适合你。你试过这种方法吗?听起来模型中的关系没有正确设置。还是 /Products(1)/Supplier 适合你? @JenS 我的控制器没有实现该方法,但这无关紧要。导航关系就在那里。请参阅上面的更新问题。 只是一个小观察:错误消息和请求 URL 引用了扩展子句中的“产品”:odata/supplier?$expand=Products。但是根据您的元数据,您必须使用“产品”。您是否尝试过,例如$expand=product? @VagifAbilov 试过了。 :/ 在这种情况下,它基于 Property 的名称;不是实体。【参考方案3】:

您应该使用ICollection 导航属性而不是IQueryable。这些类型非常不同。不确定这是您的问题,但值得解决。

【讨论】:

您在另一个评论线程中提到“即使我确实分配了 [导航属性],它仍然会因相同的错误而失败”。但是,那是在您尝试切换到 ICollection 之前。这让我想:你是怎么做到的?与.AsQueryable?如果实体中的实际导航属性也定义为IQueryable,如果您还没有这样做,我也鼓励您更改它(Relevant answer)。您看到的错误明确提到了属性的类型,这就是为什么我想确保它不是它。 没关系;您已经提到绕过您的 DTO 是有效的。【参考方案4】:

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

[Queryable(MaxExpansionDepth = 1)]

【讨论】:

你能用任何文档备份吗? MaxExpansionDepth 默认值为 1。正如您在我的问题中看到的那样,绕过 DTO 允许 $expand 在不设置此值的情况下工作。

以上是关于OData $expand、DTO 和实体框架的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 AutoMapper 将 json 请求 dto 中的 OData 枚举字符串映射到实体枚举属性

如何将OData查询与DTO映射到另一个实体?

对 DTO 的 ASP.NET WebApi OData 支持

ASP.NET C# OData 服务 + 导航属性 + $expand = null。我错过了啥?

OData 和 WebAPI:模型上不存在导航属性

WebAPI Odata 在无类型实体上应用查询选项