OData $expand, DTOs, and Entity Framework

18
我有一个基本的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。一个Product可以有一个Supplier,一个Supplier可以有多个Product。
我还创建了两个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/supplier?$expand=Products,当然会出错:

"指定的类型成员“Products”不受LINQ to Entities支持。仅支持初始化程序、实体成员和实体导航属性。"

更新: 我一直收到同样的问题,所以我添加了更多信息。是的,导航属性已经按照元数据信息正确设置。

这与控制器上缺少方法无关。如果我创建一个实现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,我会得到预期的结果。返回一个Products数组,响应中不包含Supplier字段。生成的sql查询从Suppliers表连接和选择,如果不是下一个查询结果,这对我来说是有意义的。
如果我调用/odata/product?$select=Id,我会得到我所期望的结果。但$select将转换为一个不连接到Suppliers表的sql查询。
/odata/product?$expand=Product失败并显示不同的错误:
"The argument to DbIsNullExpression must refer to a primitive, enumeration or reference type."
如果我将我的Product Controller更改为以下内容:
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();
    }
}
< p > /odata/product,/odata/product?$select=Id和/odata/product?$expand=Supplier都能正确返回结果,但显然.ToList()有点违背初衷。

我可以尝试修改Product控制器,只有在传递$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从数据库生成模型的能力? - Rafi
抱歉,我是指在DTO实例本身上。例如,在这里,似乎没有将产品列表分配给供应商:.Select(x => new Supplier() { Id = x.SupplierId, Name = x.SupplierName }); - Mike Wasson
@MikeWasson 没错。即使我分配了它,它仍然以相同的错误失败。 :/ - Schandlich
@MikeWasson,实际上我认为你可能走在了正确的道路上。 - Schandlich
1
@AmITheRWord 是和不是。我发现有许多不同的方法来解决这个问题,但没有一种是完美的。在我的当前架构中,情况变得更加复杂,因为我的 ef 实体投影到另一个 dto 中,然后我的服务再将其投影到自己的 dto 中。最终,我使用的模式并不是完全解决这个问题的答案,所以我不想在这里发布它。如果你想给我发电子邮件,我可以提供更多信息,邮箱是luke.sigler在outlook点com。 - Schandlich
显示剩余3条评论
4个回答

1

0

您的 Web 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以获取更多信息。您的模型结构非常相似。


这与我的问题不符合。/Products(1)/Supplier 不是我想要实现的目标。/Products(1)?$expand=Supplier 是我想要实现的目标。 - Schandlich
我认为关键在于/Products(1)/Supplier可能对您无效,原因与/Products(1)$expand=Supplier相同。您尝试过这种方法吗?听起来模型中的关系设置不正确。或者/Products(1)/Supplier对您有效吗? - Jen S
@JenS 我的控制器没有实现那个方法,但这与问题无关。导航关系已经存在。请参见上面更新的问题。 - Schandlich
只是一个小观察:错误消息和请求URL中的扩展子句引用了“Products”:odata/supplier?$expand=Products。但根据您的元数据,您必须使用“product”。您尝试过这个吗?例如$expand=product? - Vagif Abilov
@VagifAbilov 试过了。:/ 这是基于属性名称的,而不是实体。 - Schandlich

0

$expand 命令只有在控制器操作中添加了 MaxExpansionDepth 参数且该参数大于 0 的 Queryable 属性时才起作用。

[Queryable(MaxExpansionDepth = 1)]

你能提供任何文档来支持吗?MaxExpansionDepth的默认值为1。正如你在我的问题中所看到的,绕过DTO允许$expand在不设置此值的情况下工作。 - Schandlich

0
你应该使用一个ICollection导航属性,而不是IQueryable。这些类型非常不同。虽然不确定这是否是你的问题,但值得修复。

您在另一个评论线程中提到:“即使我分配了[navigation property],它仍然失败并显示相同的错误”。但是,在您尝试切换到ICollection之前,就是因为这个问题我想知道:您是如何做到的?使用.AsQueryable吗?如果实体中的实际导航属性也被定义为IQueryable,我还鼓励您进行更改(相关答案)。您看到的错误明确提到了该属性的类型,这就是为什么我想确保它不是导致问题的原因。 - tne
没关系,你已经提到绕过DTO可以工作了。 - tne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接