经过一组RESTful

命令和询问责任分开(CQRS)是由Greg
Young提议的一种将系统的读(查询)、写(命令)操作分离为二种独立子系统的架构模式。命令通常是异步执行的,并储存在一个事务型数据库中,而读操作则平日是最终一致的,并且数据出自于解正规化的视图。

正文在此提议并为读者显示一种为CQRS系统创制一套RESTful
API的措施。这种方法组成了HTTP的语义、REST
API基于资源的品格,并可以处理分布式总结的某些问题,例如最后一致性和并发性。

其余我们还提供了一套原型API,它确立于Greg
Young编写的m-r
CQRS原型之上,后者也被叫做SimplestPossibleThing。m-r可以认为是CQRS原型的事实标准,它刺激了众多集团利用并创办CQRS系统。即使这么些m-r原型很粗略,但它早已能够显示在实际世界中行使RESTful
CQRS系统的一点机遇和挑衅了。

我们在将下有些审阅m-r的小圈子模型,随后对相关特性的API设计举行局部钻探。最终,大家将对有的所做的取舍展开啄磨,并且琢磨一些RESTful
m-r的概念和辩解内容。

m-r领域

m-r模型是一个通过简化的库存管理体系的天地模型,你可以创制新库存物品(假若它是某序列型的制品),重命名或裁撤激活(即逻辑删除)它们。被撤销激活的物料将不再为用户所见,而所有移动的物料都足以被拿走,并且可以见到各类物品的拥有细节。你也可以扩展或裁减这一个库存物品,指定所投入或回落的物品数量。换句话说,在建立库存量之后,就可以起来运用那多少个连串了。

用户将经过共同的查询来查阅物品列表或是物品细节,对于物品状况的改动将因而命令来兑现。在实际世界中,命令应该是异步执行的,但鉴于代码中利用了内存中的轩然大波总线(伊夫(Eve)nt
Bus)及事件处理函数,因而在终极促成中命令都是一路施行的。

图片 1

m-r模型实现了CQRS:命令和询问被分别存储在不同的地点,并且各自由系统中全然两样的有些举行拍卖。

而外CQRS之外,m-r也使用了事件起点(伊夫nt
Sourcing)作为它的持久化机制。在这种方法中,对于世界模型的改动会被破获为一多级的轩然大波,那个事件会依照它们被调用的顺序存储起来。为了得到某个模型的脚下情状,需要将具备事件遵照它们发出的逐条举行重播。换句话说,模型中实体的事态音讯是不会被持久化的。举例来说,假设我们创造了一个库存物品,随后将它重命名一回,那么我们将会赢得一个InventoryItemCreated事件和五个InventoryItemRenamed事件,这多少个事件都会被保留在事件存储(伊芙(Eve)nt
Store)中。

事件是连续的,并且每个事件都蕴含一个版本号,用以在并发时举办自我批评。举例来说,假设某个库存物品在本子2的底蕴上进展重命名,但恰恰有另一个重命名发生在同一个物品上,并使它的如今版本变为3,那么那种状态就会造成出现相当。

指令与天地事件司空见惯是一定的涉嫌,当调用了某个命令之后,领域模型会倡导并储存一个风波。领域事件是事件起点的内核,它和跨四个境界上下文(bounded
context)的轩然大波不同,往往粒度更细,并且只囊括所需的矮小数量的音讯。由此,它并不是一个顺应于在不同的境界上下文之间举办合并的工具。除了利用一个进程内的风波总线之外,m-r还用到了一个内存中的轩然大波存储。这些蕴藏本质就是一个哈希表,它采用模型的id作为键,并且不断跟踪模型中暴发的任何事件。

如欲了解CQRS和事件源自的更多音讯,你可以翻阅Greg
Young的这本迷你书

创办一套上层的REST API

假诺你扶助于先去感受一下最终的贯彻,可以在此地看一下一个脚下(暂时性)可运行的原型。大家鼓励你选取fiddler或者浏览器自带的开发工具去检查一下这一个简单的以身作则中的HTTP请求。在GitHub上可以找到包括这套API和一个主导的Angular应用的源代码。但是我们仍旧要强调,它的贯彻模式和拔取的技艺毫无关键所在,读者更应当关注于规划艺术及HTTP的彰显。

公开领域的布局

对此这一个API层来说,最根本的权利是将底层的领域建模为资源,并透过HTTP语义表流露来。在这个历程中,API层将开创一个公物领域,它由资源(以及它们的唯一标识符->URL)以及输入和出口的信息所结合。底层的领域越简单,那些公开领域和底部领域的一般程度就越高。

(单击图片以加大)

图片 2

在那么些例子中,我们创设的领悟领域与底层的圈子依旧相比较一般的,但尽管是这种简易的世界,我们也不可知一向将底层的小圈子透流露来:这也许导致领域的里边贯彻被泄表露去,而且世界里面也不自然带有API层所需的任何性质。比方说,所有的中间命令都会用一个整数来代表并发时所需的版本号,而在公然领域中则用字符串表示这个特性。我们稍后将会使用这些特性作为ETag,而按照HTTP规格要求,ETag必须是不透明的。

简单易行来说,我们所成立的公开领域表现了其中的天地类,但又不完全相同。这种公然领域通常被称呼一个视图模型(Vide
Model)。这一个术语并不太可靠,因为这种表明情势感觉上对公开领域有些排斥,将它视为一种“哑”模型,由此我们赞成于拔取一个新术语“输出模型”(output
model)。它将被应用到输入和输出音讯中(命令和输出模型)。

资源

咱俩很自然地想到应该有一个InventoryItem资源,由此大家将世界中的这些单根实体透露为一个单身的资源,可以用/api/InventoryItem惠及地开展表示。每个库存物品将用/api/InventoryItem/{id}开展表示,m-r使用了大局唯一标识符(GUID)作为Id。

利用这么些独自的根对象就足以全部的显现我们的小圈子了。还有一种方法是采取/api/InventoryItem/{id}/Stock这一个资源作为丰硕和删除库存量(即签入或移除物品)的方法。从实质上说它们没有怎么高下之分,无非是哪类形式可以更好地显示资源而已。由于第一种方法越来越方便,因此我们就选取这种措施。

(单击图片以拓宽)

图片 3

查询

我们需要四个查询:GetInventoryItemsGetInventoryItemDetails。这里我们将因而多个GET方法/api/InventoryItem/api/InventoryItem/{id}显流露这六个查询效率。

GetInventoryItems方法可以取得仅包含了物品名称Id的一个列表,它会基于ACCEPT头决定回到JSON或是XML(ASP.NET
Web
API可以匡助这一效应)。假若某个资源符合于缓存,那么具有的GET请求都有可能回到缓存数据。GetInventoryItems返回InventoryItemListDataCollection作为出口信息。即使可以经过数据内容的哈希生成ETag,可是这里大家挑选将列表中每一项的Id名称举行哈希后获取的结果作为ETag再次来到给客户端(例如浏览器)。客户端可以拔取将资源缓存起来,并针对性ETag使用If-Non-Match拓展规范请求。大家采取将资源的max-age设为0,由此客户端的GET会从来使用原则请求,然则也足以采取设置一个人造的晚点时间。

GET /api/InventoryItem HTTP/1.1 
Accept:application/json, text/plain, */* 
Accept-Encoding:gzip,deflate,sdch 
If-None-Match:"LdHipfxR7BsfBI3hwqt2BLsno8ic98KmrIA1y67Nnw4="

再次来到结果

HTTP/1.1 304 Not Modified 
ETag: "LdHipfxR7BsfBI3hwqt2BLsno8ic98KmrIA1y67Nnw4="

GetInventoryItemDetails方法会重返某个库存物品的底细,包括IdNameCurrentCount属性,最终一项属性记录了当前的库存数量。即便其间领域的读取模型(read
model)包含了本子号,但一旦将某个数值类型的版本号直接作为ETag会暴发安全性问题,因为客户端可以任意地猜出下一个数值。由此,我们选拔了利用高级加密标准(AES)对版本号举办加密后,作为InventoryItemDetails方法的ETag输出。

为每个操作都再一次实现ETag对于API层来说有点负担过重,因此我们定义了一个IConcurrencyAware接口:

public interface IConcurrencyAware 
{ 
    string ConcurrencyVersion { get; set; } 
}

各样帮忙ETag的输出模型都要贯彻这些接口,当API层看到某个输出模型支撑这么些接口时,就会读取版本号并设置ETag值。另一方面,当API层对条件式GET请求举办响应时,会将转移的ETag与客户端在If-None-Match头中传入的值进行比较。所有那多少个操作都能够通过一个独门的大局filter实现:ConcurrencyAwareFilter

亟待注意的是,添加、删除或者重命名某个库存物品时应有使物品列表的缓存失效。请看上面的事例(条件式GET请求的逻辑是在浏览器端完成的,不需要专门编写代码实现):

GET /api/InventoryItem HTTP/1.1 
If-None-Match:"CWtdfNImBWZDyaPj4UjiQr/OrCDIpmjVhwp8Zjy+Ok0="

再次回到结果是一个状态码为200的完好响应,并且包含了一个新的ETag值:

HTTP/1.1 200 OK 
Cache-Control:max-age=0, private 
Content-Length:68 
ETag:"0O/961NRFDiIwvl66T1057MG4jjLaxDBZaZHD9EGeks=" 
Content-Type:application/json; charset=utf-8; domain-
model=InventoryItemListDataCollection; version=1.0.0.0; 
format=application%2fjson; schema=application%2fjson; is-text=true 
...

请留心Content-Type头包含了额外的参数,这是对此“媒体类型的五种级别”(或者简称5LMT)概念的一种实现,这种办法不是将所有信息都塞到一个独立的令牌(token)中,而是使用不同的参数来发挥对用户有用的例外级其它数码,可以抒发不同级另外有用音信。下文会对这些焦点做更加的议论。

命令

询问普通会映射到GET方法,而下令则需要映射到POST、PUT、DELETE和PATCH方法。将HTTP谓词映射到CRUD操作是一种流行的思想意识,但在真实世界中很少可以将谓词和数据库操作一一对应。实际上,REST
API并不在对持久化存储之上的一个简易包装,相反,它是指导用户去打听工作领域、操作与工作流的一扇门。因而它必须可以不依赖于特定的谓词去抒发某个维度的意图。

一种常见的点子是利用远程过程调用(RPC)风格的资源,例如/api/InventoryItem/{id}/rename。虽然它看上去确实去除了对某种谓词的依靠,但它违反了REST面向资源的显现能力。我们需要牢记,资源是一个名词,HTTP谓词则象征动词和动作,而自描述的新闻(REST的焦点之一)则是表述另外维度信息和企图的手腕。实际上,在HTTP音信中所包含的授命就应当可以描述任谁为的操作了。可是,完全倚重于请求体中的音信也有它和谐的题目,因为请求体经常是用作流传递的,要在辩认出它的具体操作在此以前拿到整个请求体有时是不容许完成的,而且这也不是一种明智的做法。那里,大家将显示一种基于5LMT中的第4级别(即世界模型)处理请求的法门,命令的花色将富含在Content-Type头中的某部参数内。

PUT /api/InventoryItem/4454c398-2fbb-4215-b986-fb7b54b62ac5 HTTP/1.1  
Accept:application/json, text/plain, */* 
Accept-Encoding:gzip,deflate,sdch 
Content-Type:application/json;domain-model=RenameInventoryItemCommand

诸如此类就可以将请求正确地输送给服务端相应的处理办法了。这这种措施是否将过多的音信外泄给客户端了啊?并非如此。输入输出音信的schema(以及名称)是通晓领域的一局部,客户端必须可以完全地拜会到它,由此它们凭借于schema也是在我们所预期的。

有关客户端的实现只用了最少量的代码,这里运用了一个AngularJS*的装饰(decorator)封装了$http服务,它亦可读取这么些原型的回到内容,并且可以在Content-Type头中出席额外的参数消息。只要保持JavaScript构造函数*的名称不变就不曾问题。

大家已经解决了识别当前正被调用的点子的问题,接下去需要将下令遵照语义映射到相应的HTTP谓词。在将指令映射到谓词时,选拔正确谓词的最首要不仅仅在于语义,同样要考虑幂等性(至于谓词的安全性则无需顾忌,因为此外一个发令谓词都是不安全的)。PUT、PATCH和DELETE是幂等的,而POST则不是幂等的(多次调用一个幂等的谓词的结果与仅调用三次是一致的)。

CreateInventoryItemCommand

从CRUD范式的角度来说,CreateInventoryItemCommand很自然地适用于POST方法。(这里只呈现重要的头音讯)

POST /api/InventoryItem HTTP/1.1 
Content-Type:application/json;domain-model=CreateInventoryItemCommand  

{"name": "CQRS Book"}

回去的响应如下:

HTTP/1.1 202 Accepted 
Location: http://localhost/SimpleCQRS.Api/api/InventoryItem/
109712b9-c3d5-4948-9947-b07382f9c8d9

该操作将在location头消息中回到那些将被成立的库存物品(因为有着操作都是异步执行的)的URL地址。

DeactivateInventoryItemCommand

有如前文所述,撤销激活库存物品就象征四次逻辑删除。其它,删除操作是幂等的,因为一再删减一个库存物品的机能和两回删除是相同的。因而我们将动用DELETE选项作为撤消激活某个物品的点子(该措施包含一个空的方法体)。

DELETE /api/InventoryItem/f2b75f21-001a-4eed-b8f3-35bf5e4e9b0d HTTP/1.1 
Content-Type:application/json;domain-model=DeactivateInventoryItemCommand  

{}

重回的响应如下:

HTTP/1.1 202 Accepted

即使也得以在方法体中传送id,但在URL中一度提供了id消息。DeactivateInventoryItemCommand构造函数的绝无仅有任务是天经地义地安装domain-model本条参数。

RenameInventoryItemCommand

RenameInventoryItemCommand比起任何命令来说更有趣一点。首先,重命名一个库存物品也就是展开改动,由此采纳PUT谓词是最合适的。另一方面,假诺你正在重命名某个物品时,你的同事也在品味将其重命名为另一个名字的话会怎样呢?这就是一个产出问题。HTTP通过If-Unmodified-SinceIf-Match提供了对资源开展并发修改时的掩护机制。因为大家利用了ETag,因而就相应地设置If-Match

PUT /api/InventoryItem/f2b75f21-001a-4eed-b8f3-35bf5e4e9b0d HTTP/1.1 
Content-Type:application/json;domain-model=RenameInventoryItemCommand 
If-Match:"DL1IsUoH709K+N5TXFzlQeQI5arO8r/U0SzXcRhuXLc="  

{"newName": "CQRS Book 1"}

AngularJs的controller会传递ETag值,并传播模型中,之后在尺度式PUT请求时开展利用。如您所见,ETag的值仅仅是对世界模型中版本号的一种表现,但我们对其进行加密以满意HTTP规格的内需。服务端获取到那一个值之后进行解密并恢复生机成版本号的数值。假使版本号不般配,领域模型就会抛出一个ConcurrencyException异常,在API层的ConcurrencyExceptionFilterAttribute类捕获到这个非常之后,会以HTTP语义的法子显示该特别。

HTTP/1.1 412 Precondition Failed

本条例子很好地表明了HTTP的出现咋样与CQRS的出现检查体制相结合。

CheckInItemsToInventoryCommand和RemoveItemsFromInventoryCommand

这六个指令就更是有意思了。我们将往库存中投入或删除一些物料。从某方面来说,这种操作是对库存物品的多寡举办更新,因而可以将其落实为一个PUT(也许PATCH更确切)方法。但因为这两个指令并非幂等(比如说,调用CheckInItemsToInventoryCommand两遍应该加上五次库存),因而最适合的谓词实际上是POST。

客户端将在Content-Type头音讯中的参数中安装领域模型的名目,如同我们前边所见的同等。

POST /api/InventoryItem/f2b75f21-001a-4eed-b8f3-35bf5e4e9b0d HTTP/1.1 
Content-Type:application/json;domain-model=CheckInItemsToInventoryCommand  

{"count": "230"}

重返的响应是千篇一律的:

HTTP/1.1 202 Accepted
HTTP的任何地点

实现HTTP的有的任啥地方方也会带来一些好处,HEAD也是一个着重的谓词,它的响应结果和GET方法一致,但回到的响应体中不包括其他内容。我们为有着GET资源都落实了HEAD谓词,例如:

HEAD /api/InventoryItem HTTP/1.1 
Accept:application/json, text/plain, */* 
Accept-Encoding:gzip,deflate,sdch

将返回

HTTP/1.1 200 OK 

ETag: "LdHipfxR7BsfBI3hwqt2BLsno8ic98KmrIA1y67Nnw4="

切切实实在促成中会将HEAD请求转向给GET方法的处理函数,而框架本身会在最终负责移除重回的始末。这一名目繁多实现都是自行触发的,由此在响应中得以正确地获取ETag。

另一个索要贯彻的严重性谓词是OPTIONS,这一个谓词可以用于生成API文档,不过咱们那边只是简短的归来该资源扶助的装有谓词:

OPTIONS /api/InventoryItem/f2b75f21-001a-4eed-b8f3-35bf5e4e9b0d HTTP/1.1

它将重返如下内容:

HTTP/1.1 200 OK 
Allow: GET,POST,OPTIONS,HEAD,DELETE,PUT 
Content-Length: 46 
Content-Type: application/json; charset=utf-8; domain-model=String%5b%5d; version=4.0.0.0; 
format=application%2fjson; schema=application%2fjson; is-text=true  

["GET","POST","OPTIONS","HEAD","DELETE","PUT"]

请留心,响应中的Allow头对于OPTIONS请求来说是必须的。然而HTTP规格本身并没有点名OPTIONS响应体中切实写法,由此我们就将同意的谓词作为一个字符串数组重临(注意,在domain-model参数中的String[]是经过UrlEncoded格局编码的结果)。可以运用这一个谓词生成符合各样schema和言语要求的API文档。

除外这一个主意之外的别样调用都会重返一个形式未找到(method not
found)
要么405状态码,ASP.NET Web API自身已经实现了这一效能:

PUT /api/InventoryItem HTTP/1.1  

{}

它将赶回:

HTTP/1.1 405 Method Not Allowed 
Allow: POST,GET,HEAD,OPTIONS  

{"message":"Http Method not supported"}

讨论

这一有些将详细讲述某些理论概念,以及我们的主宰中有些相比辛苦,或者可能引起争议的有的。

可选的产出检查

在m-r最初的贯彻中,所有命令(除了CreateInventoryItemCommand,它早已隐式地蕴藏了值为0的版本号)都包含一个平头型的CurrentVersion字段。而以此本子校官它们修改为可选的(即C#中的可空类型)。

在一边,服务端应该承担确保自身状态的完整性。由此它无法、也不应该借助于客户端所提供的版本号。并发检查是作为一个风味提供给客户端的,而不是服务端用以保证模型完整性的机制。尽管客户端关心并发行为,那它就能够拔取性地发送版本号,这曾经经过在ETag中的加密信息提供给它们了。要牢记的是,并发检查与服务端的事件版本号是见仁见智的定义,后者是服务端的其中贯彻机制。

一面,对于一些操作来说,并发检查是未曾意思的。举例来说,假如五个客户端在同一时间(调用CheckInItemsToInventoryCommand措施)添加了20个库存物品,并且它们都具有版本号n,那么内部有一个指令就会破产,但这种败北是不必要的,因为我们真正需要加上40个物品。这种问题在高访问量的情状下会被推广。想象一下,假使大气的用户涌入Amazon网站去置办哈利(哈利)波特的新型一期,在大多数处境下她们都会遇上并发问题。

在HTTP中推行PUT(和PATCH)操作时会认为出现是一个可选的检查,这一点永不偶然。即便出现检查可以异步执行,但我们需要使劲确保它必须共同实施,因此当大家回来状态码202(已接受)时,就代表服务端已经认同了没有现身冲突情况的发出。

媒体类型的五种级别(5LMT)和创制新的传媒类型

在社区里周边的一种做法是创立新的媒体类型,平日号称制作新的传媒类型。举例来说:

Content-Type:application/vnd.InventoryItemListDataCollection.1.0.0.0+json;

这种使用相当的章程表示某个媒体类型的子类型已经变成了一种通用的履行(已经实际成为一种约定了),它将子系统分解为部分特定的、或者是规范的要素,并经过+号连接在一起。已经有些经过登记的媒体类型应用了这种约定,例如application/rss+xmlapplication/atom+xml。这多少个示范处于媒体类型级别中的第3级别(或者叫做schema级别),而application/xml则处于第2级别(format级别)。某种意义上说,application/atom+xml就是一种application/xml品种,它们拔取同样的format,而前者还指明了会使用ATOM
schema。

就算这一约定会在将来版本的HTTP规格中取得认可,但它从未缓解媒体类型不断增进的问题。首先,使用任何未注册的媒体类型都是HTTP规格所不提倡的,使用上述项目标Content-Type值也是平等。实际上,假若我们需要在具备API中为四个例韩国媒体体级此外擅自组合都注册一种媒体类型,那互联网号码分配局(IANA)恐怕需要动员一大批人去专门从事这一个层面宏大的职责了。另一方面,许多客户端系统使用基于dictionary的传媒类型去处理这种请求,它们将不可能应付新创立的传媒类型。

故而利用5LMT可以允许现有的客户端继续遵照事先的章程正常干活,而更进步的客户端则足以行使更高级其它消息,它们都是作为独立的实业提供的。

经过一个公然的天地保障内部领域是关键所在

将服务端的里边贯彻举行抽象对客户端的话是非常关键的。如同往日所述,为较小的园地所开创的公然领域和中间领域会相比较一般,但即使是在m-r这些示例中,我们也不可知将内部领域直接透暴露来,而必须成立一个单身的模子,它显示了客户端可以接受和相互的消息

俺们还应有将公开领域文档化,并突显给客户端。这一端的进展值得关注,因为早已有各类不同的情势和进行起首体现水面了(从WADL到Swagger、RAML和RestDown等等)。

结论

不独通过一套REST
API暴露CQRS是唯恐的,而且HTTP语义的充裕性也使得大家可以在它的功底上编制一套流畅而使得的API。整个流程包括创制一个由命令和询问(输入输出信息)组成的当众领域,以及可以处理并发和缓存的各个资源。其它,我们还索要将中间领域的查询和下令映射为HTTP谓词,并且采取情形码以表现意况转换和非凡。使用5LMT将有助于创设完全RESTful,而不是长距离过程调用风格的资源。所有这几个都可以透过一个很小但可以运作的原型应用举行显示,该原型是通过ASP.NET
Web API和AngularJS实现的。

至于作者

图片 4Ali Kheyrollahi
是一位解决方案架构师、作者、博主、开源软件的撰稿人和进献者,目前任职于伦敦(London)的一家大型电子商务公司。他对HTTP、Web
API、REST、DDD和概念模型抱有特大的热忱。而在处理实际的业务问题上又坚韧不拔实用性。他在这一行已有12年以上的经验,并在多少个出色公司工作过。他对于电脑视觉和机具学习世界有着深厚的趣味,并且已经发表了多篇杂谈。在前头,他曾是一名医务卫生人员,并视作一名非专科医务人员工作了5年。可以在此间找到他的博客,其它他在twitter上也异常活跃,可以经过@aliostad关注他。

翻看原文地址:Exposing CQRS Through a RESTful
API

相关文章