原文链接:https://logcorner.com/building-microservices-through-event-driven-architecture-part10-handling-updates-and-deletes/
在本文中,我将讨论如何处理事件溯源系统上的更新。
在前面的步骤中,我将系统的所有业务变化存储为事件,而不是存储当前状态。我通过将所有事件应用于聚合来重建当前状态。
我已经建立了一个领域事件列表:过去发生的业务变化以通用语言表达:例如ThePackageHasBeenDeliveredToCustomer。
领域事件是不可变的,当事件发生时它不能改变。
因此,要纠正事件中的错误,我必须创建一个具有正确值的补偿事件,例如银行帐户交易。
聚合记录已提交的事件并保护业务不变量。这是事务边界。为了处理并发,我将使用带有版本控制的乐观并发控制 (OCC)。
在不获取锁的情况下,每个事务都会验证没有其他事务修改了它所读取的数据。如果数据没有改变,则提交事务,如果数据被其他人改变,则事务回滚并可以重新启动。
使用版本控制,用户读取聚合的当前状态,然后发送带有版本号的命令,如果版本号与聚合的当前版本匹配,则提交事务。
如果版本号与聚合的当前版本不匹配,在这种情况下,这意味着数据已被其他人更新。所以用户应该再次读取数据以获得正确的版本并重试。
在本教程中,我将展示如何更新语音实体。它具有以下属性:标题、描述、网址和类型。所以每个属性的更新都是一个事件,应该存储在事件存储中。
处理领域模型的更新
处理更新标题
测试用例1:当title为null或为空时,ChangeTitle应引发ArgumentNullAggregateException:
在这里,我将测试如果Title为NullOrEmpty,则系统应该引发异常。
测试用例2:当预期版本不等于聚合版本时的ChangeTitle应该引发ConcurrencyException:
在这里我将测试如果预期版本不等于聚合版本,则系统应该引发异常。
因为我创建了一个新语音,聚合版本等于0,所以如果我将expectedVersion设置为1,测试应该会引发异常。
测试用例3:具有有效参数的ChangeTitle应应用SpeechTitleChangedEvent:
在这里我将测试,如果没有错误,则应将newTitle应用于演讲的标题。换句话说:Speech.Title = “更新后新标题的值”
由于Apply函数将事件应用于聚合,因此语音的标题应等于SpeechTitleChangedEvent值的标题。
ChangeTitle最终实现:
ChangeTitle的最终实现应该是这样的。
很简单,我的标题不为空或为空,应用SpeechTitleChangedEvent。apply函数使用事件SpeechTitleChangedEvent的值设置演讲标题。
检查聚合版本的代码是在前面的步骤中开发的(参见aggregateroot.cs类)
public void ValidateVersion(long expectedVersion)
{
if (Version != expectedVersion)
{
throw new ConcurrencyException($@”Invalid version specified : expectedVersion = {Version} but originalVersion = {expectedVersion}.”);
}
}
处理更新DESCRIPTION、URL和TYPE
ChangeDescription、ChangeUrl和ChangeType应遵循与ChangeTitle相同的场景
处理申请更新
处理更新标题
测试用例1:当Command为空时处理更新应该引发ApplicationArgumentNullException :
在这里我将测试如果updateCommand为空,那么系统应该引发异常。
所以我应该模拟所有外部依赖项:IUnitOfWork、ISpeechRepository和IEventSourcingSubscriber
我将提供一个空命令并验证是否引发了ApplicationArgumentNullException。
测试用例2:当语音不存在时处理更新应该引发ApplicationNotFoundException:
这里我将测试如果要更新的语音不存在,那么系统应该引发异常(ApplicationNotFoundException)。
我必须安排我的存储库,以便它返回带有模拟的空语音:
moqEventStoreRepository.Setup(m => m.GetByIdAsync<Domain.SpeechAggregate.Speech>(command.SpeechId))
.Returns(Task.FromResult((Domain.SpeechAggregate.Speech)null));
就像这样。
测试用例3:当命令不为空时处理更新应更新语音标题:
这里我测试一下,如果命令不为空,并且数据库中存在要更新的语音,则应该更新标题。
验证语音标题是否被修改的一种方法是在将其发送到存储库之前检查它的值,它应该等于新标题的值:
moqSpeechRepository.Verify(m =>
m.UpdateAsync(It.Is<Domain.SpeechAggregate.Speech>(n =>
n.Title.Value.Equals(command.Title)
)),Times.Once);
测试用例4:当预期版本不等于聚合版本时处理更新应该引发ConcurrencyException:
在这里我将测试如果预期版本不等于聚合版本,那么系统应该引发异常。
聚合等于零,因为我实例化了一个新的语音,然后如果expectedversion不等于零,则系统应该引发ConcurrencyException。
处理仓储更新
处理更新
测试用例1:当Speech为空时处理更新应该引发RepositoryArgumentNullException :
测试用例2:当语音不存在时处理更新应该引发NotFoundRepositoryException
测试用例3:当语音有效且存在时处理更新应执行更新
以及最终的实现
处理PRESENTATION的更新
处理更新
测试用例1:当ModelState无效时更新语音应返回BadRequest:
测试用例2:发生异常时的UpdateSpeech应引发InternalServerError
同上注册语音(ExceptionMiddleware)
测试用例3:当ModelState有效且没有错误时更新语音应该返回Ok
以及最终的实现
用POSTMAN测试
按F5并启动postman和sql server。
让我们启动sql server看看发生了什么 让我们运行一个select查询,你可以看到[dbo].[Speech]和[dbo].[EventStore]这两个表是空的。
让我们启动postman并运行一个post请求来创建一个演讲:http://localhost:62694/api/speech
postman脚本在这里:LogCorner.EduSync.Command\src\Postman\BLOG.postman_collection.json
现在我应该有一个新创建的演讲和一个事件LogCorner.EduSync.Speech.Domain.Events.SpeechCreatedEvent, LogCorner.EduSync.Speech.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 请注意,版本等于 0。对于每个新语音,版本应为零。
如果我检查有效载荷,我必须看到我的事件
{
“Title”: {
“Value”: “Le Lorem Ipsum est simplement du faux texte”
},
“Url”: {
“Value”: “http://www.yahoo_1.fr”
},
“Description”: {
“Value”: “Le Lorem Ipsum est simplement du faux texte employé dans la composition et la mise en page avant impression. Le Lorem Ipsum est le faux texte standard de l’imprimerie depuis les années 1500, quand un imprimeur anonyme assembla ensemble des morceaux de texte pour réaliser un livre spécimen de polices de texte”
},
“Type”: {
“Value”: 3
},
“AggregateId”: “7c8ea8a0-1900-4616-9739-7cb008d37f74”,
“EventId”: “a688cc8a-ed56-4662-bbad-81e66ed917a0”,
“AggregateVersion”: 0,
“OcurrendOn”: “2020-01-19T15:49:59.3913833Z”
}
为了更新演讲的标题,我运行以下请求 http://localhost:62694/api/speech 这是一个PUT请求。
我拿到了新建语音的标识符CF17D255-9991-4B7B-B08E-F65B54AA9335 让我们从sql复制并将其粘贴到请求正文中。
好的,现在我可以运行put查询
回到sql server验证结果
SELECT * FROM [dbo].[Speech]
SELECT * FROM [dbo].[EventStore]
我应该看到更新的标题和一个新事件LogCorner.EduSync.Speech.Domain.Events.SpeechTitleChangedEvent。
版本应为 1,有效负载应为更新事件
{
“Title”: “UPDATE_1__Le Lorem Ipsum est simplement du faux texte”,
“AggregateId”: “7c8ea8a0-1900-4616-9739-7cb008d37f74”,
“EventId”: “de253f69-ea89-4a54-8927-e09553cc43c7”,
“AggregateVersion”: 1,
“OcurrendOn”: “2020-01-19T15:55:14.1734365Z”
}
本文的源代码可在此处获得 (Feature/Task/EventSourcingApplication)
https://github.com/logcorner/LogCorner.EduSync.Speech.Command/tree/Feature/EventSourcingHandlingUpdates