如何 POST OData 实体和 link 它同时到多个现有实体?

How to POST OData entity and link it to multiple existing entities at the same time?

我如何 post 一个实体到 OData 端点,同时将它与 body 中的其他现有实体相关联?


考虑以下 class 结构(示例):

class Invoice
{
    public int Id { get; set; }

    public Person Issuer { get; set; }

    public Person Recipient { get; set; }

    // other properties
}

class Person
{
    public int Id { get; set; }

    // other properties
}

InvoicePerson 都是我域中的实体(因此 Id 属性)。想象一下,两者都暴露在自己的实体集中,所以:

现在考虑以下要求:

I want to create a new invoice in the system that will be associated to an existing issuer and recipient

如何 "tell" 我想将给定导航 属性 关联到现有实体的 OData 框架?如何通知我的控制器操作这是意图?

理想情况下,我希望 POST body 看起来像这样:

一旦服务器收到此负载,它应该:

  1. Issuer 属性 加载所需的 "people(1)" Person。如果不存在,则返回错误请求。
  2. Recipient 属性 加载所需的 "people(2)" Person。如果不存在,则返回错误请求。
  3. 创建一个新的Invoice实例并分配上面的IssuerRecipient,然后将其保存到数据库。

我知道 OData 支持使用 entity/relation/$ref 语法配置关系 after-the-fact 与特殊 PUT/POST URL。使用这样的语法,我可以做这样的事情:

  1. POST http://host/odata/Invoices

    { "Property1": "someValue", "Property2": "100" }

  2. PUT http://host/odata/Invoices(x)/Issuer/$ref

    {"@odata.id":"http://host/odata/People(1)"}

  3. PUT http://host/odata/Invoices(x)/Recipient/$ref

    {"@odata.id":"http://host/odata/People(2)"}

但是,我希望能够在单个 POST 操作中执行所有操作,该操作应该自动创建实例。

我尝试了一些想法,看看服务器会接受什么,这似乎通过了:

{
    "Issuer": { "@odata.id": "/odata/People(1)" },
    "Recipient": { "@odata.id": "/odata/People(2)" },
    "Property1": "someValue",
    "Property2": "100",
    ...
}

但我不知道如何从中 read/parse ID(比如它是如何在专用的 Ref 方法中完成的),或者即使这在OData 标准。

现在,我将仅在模型和服务器中传递 ID 属性 假设这将始终意味着存在的关系,但这远非理想general-purpose 不够,会使我的 API 不灵活。

最简单的解决方案是直接公开模型中的 ForeignKey 属性。甚至 MS 文档中使用的模型 Entity Relations in OData v4 Using ASP.NET Web API 2.2 解释 $ref 也公开了 FK。

class Invoice
{
    public int Id { get; set; }

    public int Issuer_Person_Id { get; set; }
    [ForeignKey(nameof(Issuer_Person_Id)]
    public Person Issuer { get; set; }

    public int Recipient_Person_Id { get; set; }
    [ForeignKey(nameof(Recipient_Person_Id)]
    public Person Recipient { get; set; }

    // other properties
}

这不会使您的API不灵活,而是使您的API更多灵活。 这也使您可以更好地控制模型的 数据库实现,同时仍然与数据库引擎无关。

在启用了延迟加载的环境中,如果您需要检查相关实体的存在而不需要将其加载到内存中,那么包含 FK 会带来一些额外的性能优势。

NOTE: By including the FKs in the Model the $ref syntax and batching can still be used, but we now have access to the more practical FK Id values that can be easily validated in the server side code, just as it is easier to send the values.

现在在 PATCHPOST 中,我们可以直接使用 ID 来 link Person 记录。

The same level of information/understanding is required at both the client and server sides to achieve this, so it is still general purpose, the $metadata document fully describes which FK fields link the related entities but a good naming convention as demonstrated here can help

{
    "Issuer_Person_Id": 1,
    "Recipient_Person_Id": 2,
    "Property1": "someValue",
    "Property2": "100",
    ...
}

Be Careful:
One of the reasons that many Model designers choose NOT to expose ForeignKey properties is that ambiguity exists when or if you try to send or process both the ForeignKey and the related Entity.
For a PATCH there is no confusion, the v4.0 specification tells use specifically to ignore the related entity and that it shouldn't be sent at all.

11.4.3 Update an Entity
If an update specifies both a binding to a single-valued navigation property and a dependent property that is tied to a key property of the principal entity according to the same navigation property, then the dependent property is ignored and the relationship is updated according to the value specified in the binding.

对于 POST 但是,如果在请求和 FK 中提供了相关实体,则假定相关实体是 deep insert 并且FK 被忽略。

11.4.2.2 Create Related Entities when Creating an Entity
Each included related entity is processed observing the rules for creating an entity as if it was posted against the original target URL extended with the navigation path to this related entity.

启用 FK 因此,我的建议是在客户端采取措施,确保您不会尝试将请求中的 FK 和相关实体发送回 API。

我同意您所建议的 post 中的 @odata.id 是合乎逻辑的结论,但是它引发了其他潜在的实施问题为什么该协议提供了针对表示 ForeignKey 引用的 $ref 端点的直接 CRUD 操作的概念。

OData V4.0 是专门声明和设计的,因此针对单个资源的操作应该只影响该资源。这就是为什么我们不能在单个查询中 PATCH 相关属性,就像这个引用问题一样,有太多潜在的实现变体和 解释 他们保留了它可能如何工作规范简明扼要,按原样受到限制。

Basically a consensus between interested parties could not be reached on the protocol specifics and guidance on how to handle deep updates before the specification was drafted. The ASP.Net FX and Core implementations (as of this post) are only OData 4.0 Minimal Conformance Level OOTB. There is a lot you need to do to increase the level of conformance.

批处理是在单个事务处理查询中执行“可能”影响多个资源的操作的首选机制,但它是一个比仅公开 FK 更复杂的解决方案!


虽然我们可以使用复杂的语法和批处理以其他方式实现这一点很好,但在模型中公开 FK ID 并使客户端可以访问它们还有许多其他实际好处,而不仅仅是在服务器逻辑,IMO 这些 可以 在正确的场景中是非常大的好处:

  • 网格中的优化数据检索
    如果 很多 行有一个 link 到另一个 table 中的相同记录,您只需要从公共 [=114] 下载 linked 值=]一次。对于某些类型的数据,从公共 table 下载所有可能的查找值会更有效,然后在您的表示层中,您可以根据 ID 加入结果。在某些用例中,这些查找值可能只需要在整个会话中下载一次。

  • 组合框关系赋值
    有时间和地点,但是通过在模型中包含 FK,绑定到表示层中的 ComboBoxDropDownList 实现以更改或分配相关实体非常简单,实现是几乎与网格展示相同,将控件绑定FK,并在下拉列表中显示相关实体。

2022 年更新

OData v4.01 Minimal Conformance level应该支持深度更新
但是 .Net 5 运行时使用的当前版本的 ODataLib (v8) 不支持此功能 OOTB,并且仍然仅最低限度地兼容 v4.0,尽管具有一些比以前更高级的功能。

11.4.3.1 Update Related Entities When Updating an Entity
Payloads with an OData-Version header with a value of 4.01 or greater MAY include nested entities and entity references that specify the full set of to be related entities, or a nested delta payload representing the related entities that have been added, removed, or changed. Such a request is referred to as a “deep update”. If the nested collection is represented identical to an expanded navigation property, then the set of nested entities and entity references specified in a successful update request represents the full set of entities to be related according to that relationship and MUST NOT include added links, deleted links, or deleted entities.

json 负载实现与您的建议相似

{
    "@type":"#container.Invoice",
    "Issuer": { "@id": "People(1)" },
    "Recipient": { "@id": "People(2)" },
    "Property1": "someValue",
    "Property2": "100",
    ...
}

现在还有一个 嵌套增量表示 添加、删除或更新 link 以及单个请求中的嵌套值,但这些高级机制是尚未在 ODataLib 运行时中实现。