- 理论:让我们确保我们谈论的是同一件事。
- 在实践中:尽可能
可以(没有明确的答案)解决问题。
- 在实践中,真的(更新):实现后端特定部分的规范方式。
- 等等,它解决了我的问题吗?我们不要忘记实现 (3) 受规范 (1) 的约束。
- 或者:通常的“你真的需要它吗?” (没有明确的答案)。
理论
为了记录,以下是 OData 规范对此的说明(强调我的):
变更集中的所有操作都代表一个变更单元,因此
服务必须成功处理并应用所有请求
更改设置否则不应用任何设置。这取决于服务
定义回滚语义以撤消任何请求的实现
在可能在另一个请求之前应用的更改集中
在同一个更改集中失败,因此应用此 all-or-nothing
要求。服务可以在变更集中执行请求
以任何顺序并可以返回对单个请求的响应
以任何顺序。 (...)
http://docs.oasis-open.org/odata/odata/v4.0/cos01/part1-protocol/odata-v4.0-cos01-part1-protocol.html#_Toc372793753
这是 V4,它几乎不会更新 V3 关于批处理请求,因此同样的考虑适用于 V3 服务 AFAIK。
要理解这一点,您需要一点背景知识:
您可能会对变更集中的请求无序这一事实感到惊讶,坦率地说,我没有提供适当的理由。规范中的示例清楚地显示了相互引用的请求,这意味着必须推导出处理它们的顺序。实际上,我的猜测是变更集必须真正被视为单个请求本身(因此是原子要求),它们被一起解析并可能折叠成单个后端操作(当然取决于后端)。在大多数 SQL 数据库中,我们可以合理地启动事务并按照由它们的相互依赖关系定义的特定顺序应用每个子请求,但对于其他一些后端,可能需要在将任何更改发送到拼盘。这可以解释为什么不需要按顺序应用它们(这个概念可能对某些后端没有意义)。
这种解释的一个含义是,您的所有变更集都必须在逻辑上保持一致;例如,您不能让 PUT 和 PATCH 在同一个更改集上触及相同的属性。这将是模棱两可的。因此,客户端有责任在将请求发送到服务器之前尽可能高效地合并操作。这应该总是可行的。
(我希望有人能证实这一点。)我现在相当有信心这是正确的解释。
虽然这似乎是一种明显的良好做法,但人们通常不会这么认为批处理。我再次强调,所有这些都适用于更改集中的请求,而不是批处理请求中的请求和更改集(这些请求是有序的,并且几乎可以按照您的预期工作,减去它们的非原子/非事务性质)。
在实践中
回到您的问题,这是特定于 ASP.NET Web API 的,似乎是 they claim full support 的 OData 批处理请求。 More information here。正如您所说,似乎确实为每个子请求创建了一个新的控制器实例(好吧,我相信您的话),这反过来又带来了一个新的上下文并打破了原子性要求。那么,谁是对的呢?
好吧,正如您也正确指出的那样,如果您要在处理程序中调用SaveChanges,那么再多的框架hackery 也无济于事。看起来您应该按照我上面概述的注意事项自己处理这些子请求(注意不一致的更改集)。很明显,您需要 (1) 检测到您正在处理作为变更集一部分的子请求(以便您可以有条件地 提交)和 (2) 在调用之间保持状态。
更新:请参阅下一节,了解如何做到 (2),同时让控制器忽略功能(不需要 (1))。 如果您愿意,接下来的两段可能仍然很有趣有关HttpMessageHandler 解决方案解决的问题的更多背景信息。
我不知道您是否可以使用他们提供的当前 API 检测您是否在变更集中 (1)。我不知道您是否可以强制 ASP.NET 使控制器在 (2) 内保持活动状态。然而,你可以为后者做的(如果你不能让它活着)是保持对其他地方的上下文的引用(例如在 some kind of session state Request.Properties)并重用它有条件地(更新:或无条件地,如果您在更高级别管理事务,请参见下文)。我意识到这可能没有您希望的那么有用,但至少现在您应该有正确的问题可以直接向您的实现的开发人员/文档编写者提出。
危险地漫无边际:您可以有条件地为每个变更集创建和终止TransactionScope,而不是有条件地调用SaveChanges。这并没有消除对 (1) 或 (2) 的需求,只是另一种做事方式。它有点遵循框架可以在技术上自动实现这一点(只要可以重用相同的控制器实例),但在不了解内部原理的情况下,我不会重新审视我的说法,即框架没有足够的能力去配合自己做所有的事情。毕竟,TransactionScope 的语义对于某些后端来说可能过于具体、不相关甚至不受欢迎。
更新:这确实是正确的做事方式。下一节展示了一个使用实体框架显式事务 API 而不是 TransactionScope 的示例实现,但这具有相同的最终结果。虽然我觉得有一些方法可以实现通用实体框架,但目前 ASP.NET 不提供任何特定于 EF 的功能,因此您需要自己实现。如果您曾经提取代码以使其可重用,请尽可能在 ASP.NET 项目之外共享它(或说服 ASP.NET 团队他们应该将其包含在他们的树中)。
在实践中,真的(更新)
查看 snow_FFFFFF 的有用答案,其中引用了一个示例项目。
为了把它放在这个答案的上下文中,它展示了如何使用HttpMessageHandler 来实现我上面概述的要求#2(在单个请求中保持控制器调用之间的状态)。这通过在比控制器更高的级别上挂钩来工作,并将请求拆分为多个“子请求”,同时保持状态对控制器(事务)不知情,甚至将状态暴露给控制器(实体框架上下文,在此案例通过HttpRequestMessage.Properties)。控制器会愉快地处理每个子请求,而不知道它们是普通请求、批处理请求的一部分,还是变更集的一部分。他们需要做的就是在请求的属性中使用实体框架上下文,而不是使用他们自己的。
请注意,您实际上有很多内置支持来实现这一点。这个实现建立在DefaultODataBatchHandler之上,它建立在ODataBatchHandler代码之上,它建立在HttpBatchHandler代码之上,这是一个HttpMessageHandler。相关请求使用Routes.MapODataServiceRoute 显式路由到该处理程序。
这个实现如何映射到理论?很好,其实。您可以看到,如果每个子请求是“操作”(正常请求),则发送每个子请求以由相关控制器按原样处理,或者如果它是变更集,则由更具体的代码处理。在此级别,它们按顺序处理,但不是原子处理。
然而,变更集处理代码确实将它自己的每个子请求包装在一个事务中(每个变更集一个事务)。虽然此时代码可以通过查看每个子请求的 Content-ID 标头来构建依赖关系图来尝试找出在事务中执行语句的顺序,但此实现采用更直接的方法,即要求客户端以正确的顺序对这些子请求进行排序,这很公平。
等等,它解决了我的问题吗?
如果您可以将所有操作包装在一个变更集中,那么可以,请求将是事务性的。如果不能,则必须修改此实现,以便将整个批次包装在单个事务中。虽然规范应该不排除这一点,但有明显的性能考虑需要考虑。您还可以添加一个非标准的 HTTP 标头来标记您是否希望批处理请求是事务性的,并让您的实现采取相应的行动。
无论如何,这都不是标准的,如果您想以可互操作的方式使用其他 OData 服务器,则不能指望它。要解决此问题,您需要向 OASIS 的 OData 委员会提出可选的原子批处理请求。
或者
如果您在处理变更集时找不到分支代码的方法,或者您无法说服开发人员为您提供这样做的方法,或者您无法保持变更集特定的状态以任何令人满意的方式,那么看起来您必须 [您可能希望] 公开一个全新的 HTTP 资源,其语义特定于您需要执行的操作。
您可能知道这一点,这很可能是您想要避免的,但这涉及使用 DTO(数据传输对象)来填充请求中的数据。然后,您解释这些 DTO 以在单个 handler 控制器操作中操作您的实体,从而完全控制结果操作的原子性。
请注意,有些人实际上更喜欢这种方法(更多面向过程,更少面向数据),尽管它可能很难建模。没有正确的答案,它总是取决于领域和用例,而且很容易陷入使您的 API 不是非常 RESTful 的陷阱。这是 API 设计的艺术。 不相关:关于数据建模也可以这样说,有些人实际上觉得这更难。 YMMV。
总结
有几种方法可供探索,从开发人员那里检索一些信息使用的规范实现技术,创建通用实体框架实现的机会,以及非通用替代方案。 p>
如果你能在其他地方收集答案(好吧,如果你觉得有足够的动力)以及你最终决定做的事情时更新这个帖子会很好,因为这似乎是很多人喜欢做的事情有某种明确的指导。
祝你好运;)。