由此同样组RESTful API暴露CQRS系统功能

指令和查询责任分开(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模型是一个由此简化的库存管理体系的小圈子模型,你可创建新库存物品(假设它是某种类型的活),重命名或撤销激活(即逻辑删除)它们。被收回激活的物品拿不再为用户所表现,而颇具活动之物料都可以为得,并且能看出每个物品的装有细节。你啊克多或回落这些库存物品,指定所投入或者减少的物品数量。换句话说,在成立库存量之后,就得起采用这体系了。

用户用经合的查询来查阅物品列表或是物品细节,对于物品状态的改将经过命令来贯彻。在现实世界中,命令应该是异步执行之,但鉴于代码中动用了内存中的轩然大波总线(Event
Bus)及事件处理函数,因此在终极兑现着命令还是联名实施的。

bifa365必发 1

m-r模型实现了CQRS:命令和查询被分别存储于不同之地方,并且各自由系统中全然两样之片段进行拍卖。

除CQRS之外,m-r也使用了事件起源(Event
Sourcing)作为其的持久化机制。在这种措施面临,对于世界模型的修改会于擒获为同一密密麻麻的轩然大波,这些事件会照它被调用的顺序存储起来。为了获取有模型的即状态,需要将具有事件仍她有的一一进行重播。换句话说,模型中实体的状态信息是未见面被持久化的。举例来说,如果我们创建了一个库存物品,随后将它们再命名两蹩脚,那么我们用会见获得一个InventoryItemCreated事件以及片个InventoryItemRenamed事件,这些事件都见面让保存在事件存储(Event
Store)中。

事件是接二连三的,并且每个事件还含一个本子号,用以在连发时进行自我批评。举例来说,如果某库存物品在本2底功底及开展再命名,但巧有其它一个再次命名发生在和一个物品达,并设其的眼前本变为3,那么这种状态便见面促成出现异常。

一声令下和天地事件便是相当的涉,当调用了某命令后,领域模型会发起并蕴藏一个事件。领域事件是事件起源的内核,它与越多个边界及下文(bounded
context)的事件不同,往往粒度更仔细,并且就包括所要的极端小数目的消息。因此,它并无是一个合吃当不同之疆界及下文之间进行合并的家伙。除了利用一个经过内的波总线之外,m-r还因此到了一个外存中的风波存储。这个蕴藏本质就是是一个哈希表,它采用模型的id作为键,并且不断跟踪模型中产生的其余事件。

倘得询问CQRS和波起源的复多信息,你可翻阅Greg
Young的当时按照迷你写。

缔造同拟上层之REST API

如若您赞同于事先夺感受一下最终的兑现,可以每当此处关押一下一个当下(暂时性)可运行的原型。我们鼓励公以fiddler或者浏览器自带的开发工具去检查一下这个简单的以身作则中之HTTP请求。在GitHub及足找到包这套API和一个着力的Angular应用之源代码。不过我们或如强调,它的兑现方式同运的技术并非要所在,读者更当关注为规划方以及HTTP的变现。

光天化日领域的布局

对这个API层来说,最关键的义务是将脚的领域建模为资源,并通过HTTP语义暴露出来。在这历程遭到,API层将创设一个公共领域,它由资源(以及她的绝无仅有标识符->URL)以及输入和出口的信息所结合。底层的世界更是简单,这个公开领域和底领域的形似程度就进一步强。

(单击图片以拓宽)

bifa365必发 2

每当是事例中,我们创建的公然领域及底层的世界要于一般之,但即使是这种简单的天地,我们呢无可知直接拿脚的圈子暴露出来:这恐怕导致领域的中贯彻为外泄出来,而且世界内也未自然带有API层所欲的一切性能。比方说,所有的里命令还见面就此一个整数来表示并发时所要的版本号,而当公然领域面临尽管就此字符串意味着这特性。我们聊晚拿会动用是特性作为ETag,而据悉HTTP规格要求,ETag必须是匪透明的。

粗略来说,我们所创办的明领域表现了中间的领域接近,但同时不完全相同。这种公然领域通常给称之为一个视图模型(Vide
Model)。这个术语并无极端可靠,因为这种表达方式感觉上针对公开领域有些排外,将她视为等同栽“哑”模型,因此我们赞成被采取一个新术语“输出模型”(output
model)。它以让以至输入和出口消息被(命令和出口模型)。

资源

我们老当然地想到该发一个InventoryItem资源,因此我们用世界中的此单根实体暴露也一个单身的资源,可以据此/api/InventoryItem利地展开表示。每个库存物品拿故/api/InventoryItem/{id}进行表示,m-r以了大局唯一标识符(GUID)作为Id。

下这个独自的到底对象就可以整体的表现我们的圈子了。还有同种方法是利用/api/InventoryItem/{id}/Stock是资源作为丰富和去库存量(即签入或移除物品)的主意。从本质上说它没有什么高下的分,无非是哪种艺术会再次好地见资源而已。由于第一种方法更为方便,因此我们就是用这种措施。

(单击图片以放开)

bifa365必发 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。

客户端将在bifa365必发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单物品。这种题材在高访问量的景象下会给加大。想象一下,如果大气之用户涌入亚马逊网站去买哈利波特的流行一愿意,在大多数状态下她们都见面遇到并发问题。

于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实现之。

有关作者

bifa365必发 4Ali Kheyrollahi
是均等各项解决方案架构师、作者、博主、开源软件之作者及贡献者,目前供职于伦敦的一样贱大型电子商务企业。他针对性HTTP、Web
API、REST、DDD和概念模型抱出特大的来者不拒。而当拍卖实际的业务问题及同时坚持实用性。他于当时无异于尽既生12年以上的经历,并于差不多独优秀企业工作了。他对于电脑视觉与机械上园地拥有深厚的志趣,并且都公布了大半首论文。在前面,他曾是均等名叫医生,并视作一如既往叫做不专科医生工作了5年。可以当此地找到他的博客,此外他在twitter上为特别活跃,可以经过@aliostad关注他。

查阅原文地址:Exposing CQRS Through a RESTful
API

发表评论

电子邮件地址不会被公开。 必填项已用*标注