AngularJS由此一组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模型是一个经过简化的库存管理序列的世界模型,你可以创建新库存物品(假使它是某连串型的产品),重命名或收回激活(即逻辑删除)它们。被注销激活的物品将不再为用户所见,而富有活动的物料都得以被拿走,并且可以看到各样物品的有所细节。你也可以扩展或减弱这个库存物品,指定所进入或回落的物料数量。换句话说,在确立库存量之后,就可以起始使用那个体系了。

用户将由此联合的询问来查看物品列表或是物品细节,对于物品状况的改动将经过命令来实现。在切切实实世界中,命令应该是异步执行的,但出于代码中接纳了内存中的事件总线(伊芙(Eve)nt
Bus)及事件处理函数,因而在最后兑现中命令都是一同施行的。

AngularJS 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)以及输入和出口的音信所结合。底层的领域越简单,这多少个公开领域和底部领域的相似程度就越高。

(单击图片以推广)

AngularJS 2

在这一个事例中,我们创造的当众领域与底层的圈子仍然相比相似的,但尽管是这种简易的小圈子,我们也不可知向来将底层的小圈子表表露来:这也许导致领域的内部贯彻被泄透露来,而且世界里面也不肯定带有API层所需的整整特性。比方说,所有的里边命令都会用一个整数来表示并发时所需的版本号,而在当众领域中则用字符串代表那一个特性。我们稍后将会动用这么些特性作为ETag,而基于HTTP规格要求,ETag必须是不透明的。

简言之的话,我们所开创的公开领域表现了中间的天地类,但又不完全相同。这种公开领域平常被称作一个视图模型(Vide
Model)。那么些术语并不太标准,因为这种表明形式感觉上对公开领域有些排外,将它视为一种“哑”模型,由此大家赞成于拔取一个新术语“输出模型”(output
model)。它将被运用到输入和输出信息中(命令和出口模型)。

资源

大家很自然地想到应该有一个InventoryItem资源,因而我们将世界中的这么些单根实体表露为一个独立的资源,可以用/api/InventoryItem有利地拓展表示。每个库存物品将用/api/InventoryItem/{id}进展表示,m-r使用了大局唯一标识符(GUID)作为Id。

选取那多少个独自的根对象就可以完整的突显我们的天地了。还有一种办法是使用/api/InventoryItem/{id}/Stock以此资源作为充裕和删除库存量(即签入或移除物品)的法门。从精神上说它们并未什么样高下之分,无非是哪一种办法可以更好地显现资源而已。由于第一种艺术更为简便易行,因而大家就选拔那种方法。

(单击图片以加大)

AngularJS 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实现的。

至于作者

AngularJS 4Ali Kheyrollahi
是一位解决方案架构师、作者、博主、开源软件的作者和贡献者,近年来供职于伦敦(London)的一家大型电子商务集团。他对HTTP、Web
API、REST、DDD和概念模型抱有大幅度的热心肠。而在处理实际的事务问题上又坚贞不屈实用性。他在这一行已有12年以上的阅历,并在五个非凡集团工作过。他对于电脑视觉和机器学习世界有着坚实的兴趣,并且已经发布了多篇杂谈。在头里,他曾是一名医师,并作为一名非专科医务人员工作了5年。能够在此地找到他的博客,另外她在twitter上也异常活跃,可以经过@aliostad关注他。

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

相关文章