通过 RESTful API提供CQRS

这是基于Greg Young的CQRS m-r 原型,将其通过RESTFul暴露给外界,提供API服务。

M-R模型是一个仓库系统的简化,你能创建或修改或失效(逻辑删除)一个仓库条目,可以简化认为它是产品类型。

查看条目的详细信息列表,是通过同步的查询来实现。条目状态的修改是使用命令,其在现实世界中是使用异步实现,当然因为使用了in-process事件总线,最终两者是同步的。

用例图如下:

m-r使用Event Sourcing实现持久机制,任何领域模型的修改被截获成一系列事件,然后顺序地存储起来。也就是说,实体的状态没有持久保存。

事件是顺序的,每个事件有一个版本号,能用于并发检查,如果两个操作对同一个项目同时改名,但是版本不同,抛出并发错误(类似乐观锁)。

命令通常是和实现了命令的领域事件一对一的,领域模型发出事件将被保存,领域事件是EventSourcing的基础,事件是颗粒状,包含少量信息,并不是上下文集成的工具,对于EventBus,m-r使用内存in-memory EventStore,这个存储是一个hashmap,key是模型的id,而值是事件对象。

详细可见:http://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

现在我们通过RESTful将这个m-r系统暴露出来,提供API服务,具体运行演示可见:http://m-r.azurewebsites.net/index.html#/

结合Angular的源码项目在:https://github.com/aliostad/m-r

API层最重要的目标是将内部核心领域模型作为资源,并与HTTP语义衔接。在此过程中,API层创建了一个公开领域,它由资源( URL)和输入/输出消息组成。核心领域越简单,对外的公开领域就能更简单贴近核心领域。

在这里,公开领域非常相似核心领域模型,但是我们不能自己将核心领域模型公开,比如内部所有的命令都有一个整数类型的version属性用于并发控制,而在对外公开领域,这个属性是一个字符串,可能用于Etag等,是不透明的。

下图是对外公开的资源表,详细注明了输入输出。

(banq注:使用命令的并发版本号作为输出的Etag标记,每一次修改写操作必然是由命令引起的,因此每次写操作必然有一个新的版本号,版本号不但可用来解决并发,而且用来作为http协议中的缓存标记,每次浏览器客户端都会和服务器比较etag是否一致,如果一致就表示数据没有修改,服务器返回304,那么查询的页面还是之前的。具体可见我的一篇文章:
http://www.jdon.com/40381)

这里就不重复翻译了。

查询自然应对了REST的GET,而命令与POST PUT和DELETE映射,将Http动词映射到CRUD是大部分的思路,但是现实中在动词和数据库的操作之间一对一映射的情况很少,REST API不是存储数据库的外壳,相反,它是有着业务操作和流程的丰富的业务领域的门户,它必须不借助于谓词能表达一种意图的核心。(说白了:REST=名称+谓词 不是 动词+名词,顺序正好相反,比如产品创建,而不是 “创建产品”,后者是一种RPC风格,不是REST)。

RPC风格是 /api/InventoryItem/{id}/rename,这是就不再需要POST PUT等REST的任何动词了,因为rename这个动词已经占据主要位置(动词第一,而不是名称第一,资源是一种名称),这是对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装饰器就可以将$http服务包装,然后增加一些对Content-Type头部信息读取的动作。只要将javascript的构造器函数名称和资源名称一致即可,现在我们已经解决了识别调用的操作的问题。

比如我们新增一个条目:
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

返回的url是新资源被创建的标识(我们可以再次对这个URL发出get获得确认等等显示)

下面是修改的命令:
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"}

http提供了一个并发修改资源的机制,使用Etag的If-Unmodified-Since和 If-Match,这里使用后者。
Etag会被AngularJS控制器发现,然后加到模型中,用于后来的PUT,Etag的值只是领域模型的版本号,但是加密了,这个值会被后端获得解密转换为版本号,如果版本号不匹配,领域将发出一个并发修改的出错ConcurrencyException 。这将被API层的ConcurrencyExceptionFilterAttribute 获得,表达为HTTP错误:
HTTP/1.1 412 Precondition Failed

这是一个HTTP并发和CQRS并发多么完美结合的例子啊。

以上是原文主要思想翻译,源码下载链接见文章开始,原文英文可猛击标题。