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

Posted

技术标签:

【中文标题】OData 和 WebAPI:模型上不存在导航属性【英文标题】:OData and WebAPI: Navigation property not present on model 【发布时间】:2015-08-02 03:15:42 【问题描述】:

我正在尝试使用实体框架、WebAPI、OData 和 Angular 客户端组合一个简单的玩具项目。一切正常,除了我放在我的一个模型上的导航属性似乎没有工作。当我使用 $expand 调用我的 API 时,返回的实体没有导航属性。

我的班级是 Dog 和 Owner,看起来像这样:

    public class Dog

    // Properties
    [Key]
    public Guid Id  get; set; 
    public String Name  get; set; 
    [Required]
    public DogBreed Breed  get; set; 
    public int Age  get; set; 
    public int Weight  get; set; 


    // Foreign Keys
    [ForeignKey("Owner")]
    public Guid OwnerId  get; set; 

    // Navigation
    public virtual Owner Owner  get; set; 


    public class Owner

    // Properties
    public Guid Id  get; set; 
    public string Name  get; set; 
    public string Address  get; set; 
    public string Phone  get; set; 
    public DateTime SignupDate  get; set; 

    // Navigation
    public virtual ICollection<Dog> Dogs  get; set;  

我还设置了我的 Dog 控制器来处理查询:

public class DogsController : ODataController

    DogHotelAPIContext db = new DogHotelAPIContext();
    #region Public methods 

    [Queryable(AllowedQueryOptions = System.Web.Http.OData.Query.AllowedQueryOptions.All)]
    public IQueryable<Dog> Get()
    
        var result =  db.Dogs.AsQueryable();
        return result;
    

    [Queryable(AllowedQueryOptions = System.Web.Http.OData.Query.AllowedQueryOptions.All)]
    public SingleResult<Dog> Get([FromODataUri] Guid key)
    
        IQueryable<Dog> result = db.Dogs.Where(d => d.Id == key).AsQueryable().Include("Owner");
        return SingleResult.Create(result);
    

    protected override void Dispose(bool disposing)
    
        db.Dispose();
        base.Dispose(disposing);
    


我已经在数据库中植入了一些示例数据。所有狗记录都有一个 OwnerId,它与 Owners 表中所有者的 ID 相匹配。

使用此方法查询狗列表效果很好:

http://localhost:49382/odata/Dogs

我得到一个 Dog 实体列表,没有 Owner 导航属性。

使用 OData $expand 与主人一起查询狗不起作用:

http://localhost:49382/odata/Dogs?$expand=Owner

我的响应是包含所有 Dog 实体的 200,但它们都没有 JSON 中的 Owner 属性。

如果我查询我的元数据,我发现 OData 似乎确实知道它:

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
  <edmx:DataServices>
    <Schema Namespace="DogHotelAPI.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityType Name="Dog">
        <Key>
          <PropertyRef Name="id" />
        </Key>
        <Property Name="id" Type="Edm.Guid" Nullable="false" />
        <Property Name="name" Type="Edm.String" />
        <Property Name="breed" Type="DogHotelAPI.Models.Enums.DogBreed" Nullable="false" />
        <Property Name="age" Type="Edm.Int32" Nullable="false" />
        <Property Name="weight" Type="Edm.Int32" Nullable="false" />
        <Property Name="ownerId" Type="Edm.Guid" />
        <NavigationProperty Name="owner" Type="DogHotelAPI.Models.Owner">
          <ReferentialConstraint Property="ownerId" ReferencedProperty="id" />
        </NavigationProperty>
      </EntityType>
      <EntityType Name="Owner">
        <Key>
          <PropertyRef Name="id" />
        </Key>
        <Property Name="id" Type="Edm.Guid" Nullable="false" />
        <Property Name="name" Type="Edm.String" />
        <Property Name="address" Type="Edm.String" />
        <Property Name="phone" Type="Edm.String" />
        <Property Name="signupDate" Type="Edm.DateTimeOffset" Nullable="false" />
        <NavigationProperty Name="dogs" Type="Collection(DogHotelAPI.Models.Dog)" />
      </EntityType>
    </Schema>
    <Schema Namespace="DogHotelAPI.Models.Enums" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EnumType Name="DogBreed">
        <Member Name="AfghanHound" Value="0" />
        <Member Name="AmericanStaffordshireTerrier" Value="1" />
        <Member Name="Boxer" Value="2" />
        <Member Name="Chihuahua" Value="3" />
        <Member Name="Dachsund" Value="4" />
        <Member Name="GermanShepherd" Value="5" />
        <Member Name="GoldenRetriever" Value="6" />
        <Member Name="Greyhound" Value="7" />
        <Member Name="ItalianGreyhound" Value="8" />
        <Member Name="Labrador" Value="9" />
        <Member Name="Pomeranian" Value="10" />
        <Member Name="Poodle" Value="11" />
        <Member Name="ToyPoodle" Value="12" />
        <Member Name="ShihTzu" Value="13" />
        <Member Name="YorkshireTerrier" Value="14" />
      </EnumType>
    </Schema>
    <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityContainer Name="Container">
        <EntitySet Name="Dogs" EntityType="DogHotelAPI.Models.Dog">
          <NavigationPropertyBinding Path="owner" Target="Owners" />
        </EntitySet>
        <EntitySet Name="Owners" EntityType="DogHotelAPI.Models.Owner">
          <NavigationPropertyBinding Path="dogs" Target="Dogs" />
        </EntitySet>
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>

我可能遗漏了什么会阻止我的导航预操作与我的模型的其余部分一起返回?

编辑

为了进一步隔离问题,我尝试在服务器端包含 C# 中的 Owners。我在我的 Dog 控制器的 Get 方法中添加了这一行:

var test = db.Dogs.Include("Owner").ToList();

有了这个我可以调试并看到相关的所有者被包括在内。在此列表中,每只狗都有与之关联的主人。

对实际返回的内容使用 .Include("Owner") 并不能解决问题 - 属性仍然永远不会到达客户端。

这似乎意味着导航属性正在工作,但没有被发送回客户端。我猜这似乎表明 OData 或 WebAPI 存在问题,但我不确定是什么。

此外,我还在 Global.asax 文件中的 Application_Start 中添加了以下几行,以处理循环导航属性:

            var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
        json.SerializerSettings.PreserveReferencesHandling =
            Newtonsoft.Json.PreserveReferencesHandling.All;

我这样做是为了以防循环引用在某种程度上是罪魁祸首,但这没有任何改变。

更新

我注意到打电话给

http://localhost:49382/odata/Dogs(abfd26a5-14d8-4b14-adbe-0a0c0ef392a7)/owner

有效。这将检索与该狗关联的所有者。这进一步说明我的导航属性设置正确,它们只是没有包含在使用 $expand 的调用响应中。

更新 2

这里是我的WebApiConfig文件的注册方法:

        public static void Register(HttpConfiguration config)
    
        //config.Routes.MapHttpRoute(
        //    name: "DefaultApi",
        //    routeTemplate: "api/controller/id",
        //    defaults: new  id = RouteParameter.Optional 
        //);

        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EnableLowerCamelCase();
        builder.EntitySet<Dog>("Dogs");
        builder.EntitySet<Owner>("Owners");

        config.EnableQuerySupport();

        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: "odata",
            model: builder.GetEdmModel());


        // Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
        // To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
        // For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
        //config.EnableQuerySupport();

        // To disable tracing in your application, please comment out or remove the following line of code
        // For more information, refer to: http://www.asp.net/web-api
        config.EnableSystemDiagnosticsTracing();
    

【问题讨论】:

在我自己的功能性 OData v4 设置上进行测试。我发现放置 include("RelatedEntity") 也没有返回相关实体。我试图研究如何在 $expand 命令被传递到 API 后对其进行调试,但无法找到该信息。我建议是否可以使用 Visual Studio 提供的 Scaffolding 来生成 Owner 和 Dog 控制器。调整您的 [Queryable] 以满足您对生成的通用的要求,并尽可能尝试这种方式。 我仍然认为它可能只是与导航属性和外键设置有关。也许可以尝试在带有断点的单独类中进行非 OData 查询,以查看是否可以提取两个数据点以验证是否使用通用实体查询设置了关系。 【参考方案1】:

看看下面的内容是否适合你。我正在 OData v4 中进行测试,因此您可能需要将 [EnableQuery] 调整为 [Queryable]。您的数据库上下文应该返回一个 IQueryable 结果,这样可能就不需要 .AsQueryable()

// GET: odata/Dogs
[EnableQuery]
public IQueryable<Dog> Get()

    return db.Dogs;


// GET: odata/Dogs(5)/Owner
[EnableQuery]
public IQueryable<Owner> GetOwner([FromODataUri] int key)

    return db.Dogs.Where(m => m.ID == key).SelectMany(m => m.Owner);

我正在将您所拥有的内容与我目前正在进行的一个小项目进行比较。情况可能并非如此,但我的 FK 关联的设置略有不同,并且可能出于某种侥幸,FK 的排序是问题所在。我的外键似乎装饰在导航属性之上。

public int PublicImageID  get; set; 
[ForeignKey("PublicImageID")]
public PublicImage PublicImage  get; set; 

// Foreign Keys    
public Guid OwnerId  get; set; 
[ForeignKey("OwnerId")]
public virtual Owner Owner  get; set; 

【讨论】:

我试过这个,我仍然看到同样的事情。请注意,我必须使用 Queryable,然后在其中明确允许查询选项,以便获得除错误消息以外的任何内容。 对我来说也很有趣的是,即使我在服务器端控制器中使用 .Include("Owner"),在我的响应中也不会将 Owner 属性与 Dog 实体一起发送。 我正在比较一些非常相似的功能代码。我添加了一个我的外键声明的例子,看看是否出于某种侥幸,这可能是罪魁祸首。 该死!我希望就是这样。进行这些更改后,Dogs 仍然会在没有 Owner 属性的情况下返回。这仍然令人困惑,因为我觉得我正在采用“开箱即用”的方式进行设置。 哦,好吧,我怀疑是这种情况,但是您是否也有一个所有者控制器,您可以为其拉取所有者?【参考方案2】:

我有一个非常相似的问题,我认为这是由完全相同的问题引起的。

我试图创建一些绑定的 OData 函数,这些函数将返回整个实体图,以使客户端在某些情况下工作更轻松,而不必为所有内容指定 $expand 子句。

我在实体框架 linq 调用中指定了 Include 语句,并验证了返回数据确实在调试中完全填充,但是,像你一样,我只得到了返回的***实体,没有别的。

问题在于用于 OData 的序列化

您会发现,如果您从 Owner 类中删除主键,使其本质上成为一个复杂实体,那么它将包含在 OData 序列化 JSON 结果中,否则除非 OData 请求 uri 包含一个包含它的 $expand 子句。

我试图找到一种在代码中插入 $expand 子句以使其工作的方法,但不幸的是出现了空白。

希望对你有帮助

【讨论】:

感谢您的评论。但是,我不确定它与我的问题到底有什么关系:我已经没有在 Owner 类上指定 [Key],并且我正在使用 $expand 子句来将它与我的 Dog 实体一起使用。你到底建议我改变什么? 怀疑你会发现 [Key] 是推断出来的,因为你有一个 Id 属性 - 如果你删除这个属性,它会起作用。 我无法验证这一点,因为如果我这样做了,那么 config.MapODataServiceRoute 会出现错误,指出缺少密钥。即使没有,虽然这可能会更清楚地说明潜在问题,但距离成为一个好的解决方案还有很长的路要走。大量的 WebAPI + OData 教程都有在多个实体上使用键调用 $expand 的示例。 你的权利它根本不是一个好的解决方案,我没有建议它。我没有意识到你也不能让 $expand 工作,这很奇怪。我以为您只是希望 Owner 包含在 SingleResult Get 的 JSON 结果中,因为您已经使用 Include 语句急切地将其加载到 linq 调用中。 Liek 我说过 - 这是我想做的事情,除非实体是一个复杂的对象并且没有自己的键,否则无法让它工作。 Ahhhhhh - 现在很清楚 builder.EnableLowerCamelCase(); - 这是罪魁祸首,由于大写,它不承认所有者。如果你这样做 /Dogs?$expand=owner 会起作用【参考方案3】:

这是因为你正在使用

builder.EnableLowerCamelCase();

在您的 ODataConventionModelBuilder 设置中。

它无法识别您的查询选项 $expand 子句中的“所有者”,因为 OData 模型中确实不存在该路径,因为它区分大小写。

如果您尝试请求此 /Dogs?$expand=owner,我相信这会起作用,并且您会在 JSON 响应中返回 Dogs 及其所有者。

【讨论】:

对不起 - 我知道这很令人生气,但事实并非如此。 :) 使用 $expand=owner 不会改变任何东西。但是,现在我已将控制器方法上的 [Queryable] 属性更改为 [EnableQuery],使用 expand 会导致错误:“URI 中指定的查询无效。在类型上找不到名为 'Owner' 的属性'DogHotelAPI.Models.Dog'。” 此外,我不认为这是扩展格式的问题,因为“所有者”字段永远不会在 Dog 实体上返回。我希望即使是对所有 Dogs 的简单查询也会返回它们的“所有者”实体,即使它为空。这不会发生 - “所有者”永远不会出现在返回的 Dog 实体上。【参考方案4】:

我找到了解决问题的方法,这最终是由三件事引起的:

1.) 我在我的控制器方法上使用了 [Queryable] 属性,这些方法已被弃用。我需要使用较新的 [EnableQuery] 属性。

2.) 在我的 WebApiConfig.cs 文件中,我使用默认的 config.EnableQuerySupport() 启用查询。这已被弃用,并已被删除

3.) 我需要的展开调用采用 $expand=Owner 的形式,但需要采用 $expand=owner 的形式,因为我在 ODataConventionModelBuilder 上启用了小驼峰式大小写。非常感谢 Mark Bennetts,他的回答指出了这一点!

进行所有这些更改后,相关的 Owner 实体将与 Dog 实体一起返回。

【讨论】:

以上是关于OData 和 WebAPI:模型上不存在导航属性的主要内容,如果未能解决你的问题,请参考以下文章

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

WebApi oData v3 实体数据模型路由

打字稿:类型上不存在属性“导航”省略反应导航v5

“导航器”类型上不存在属性“共享”

错误:“导航器”类型上不存在属性“通知”

错误 TS2339:“导航器”类型上不存在属性“相机”