基于事件驱动架构构建微服务第9部分:处理更新
原文鏈接:https://logcorner.com/building-microservices-through-event-driven-architecture-part10-handling-updates-and-deletes/
在本文中,我將討論如何處理事件溯源系統(tǒng)上的更新。
在前面的步驟中,我將系統(tǒng)的所有業(yè)務(wù)變化存儲為事件,而不是存儲當(dāng)前狀態(tài)。我通過將所有事件應(yīng)用于聚合來重建當(dāng)前狀態(tài)。
我已經(jīng)建立了一個(gè)領(lǐng)域事件列表:過去發(fā)生的業(yè)務(wù)變化以通用語言表達(dá):例如ThePackageHasBeenDeliveredToCustomer。
領(lǐng)域事件是不可變的,當(dāng)事件發(fā)生時(shí)它不能改變。
因此,要糾正事件中的錯(cuò)誤,我必須創(chuàng)建一個(gè)具有正確值的補(bǔ)償事件,例如銀行帳戶交易。
聚合記錄已提交的事件并保護(hù)業(yè)務(wù)不變量。這是事務(wù)邊界。為了處理并發(fā),我將使用帶有版本控制的樂觀并發(fā)控制 (OCC)。
在不獲取鎖的情況下,每個(gè)事務(wù)都會驗(yàn)證沒有其他事務(wù)修改了它所讀取的數(shù)據(jù)。如果數(shù)據(jù)沒有改變,則提交事務(wù),如果數(shù)據(jù)被其他人改變,則事務(wù)回滾并可以重新啟動(dòng)。
使用版本控制,用戶讀取聚合的當(dāng)前狀態(tài),然后發(fā)送帶有版本號的命令,如果版本號與聚合的當(dāng)前版本匹配,則提交事務(wù)。
如果版本號與聚合的當(dāng)前版本不匹配,在這種情況下,這意味著數(shù)據(jù)已被其他人更新。所以用戶應(yīng)該再次讀取數(shù)據(jù)以獲得正確的版本并重試。
在本教程中,我將展示如何更新語音實(shí)體。它具有以下屬性:標(biāo)題、描述、網(wǎng)址和類型。所以每個(gè)屬性的更新都是一個(gè)事件,應(yīng)該存儲在事件存儲中。
處理領(lǐng)域模型的更新
處理更新標(biāo)題
測試用例1:當(dāng)title為null或?yàn)榭諘r(shí),ChangeTitle應(yīng)引發(fā)ArgumentNullAggregateException:
在這里,我將測試如果Title為NullOrEmpty,則系統(tǒng)應(yīng)該引發(fā)異常。
測試用例2:當(dāng)預(yù)期版本不等于聚合版本時(shí)的ChangeTitle應(yīng)該引發(fā)ConcurrencyException:
在這里我將測試如果預(yù)期版本不等于聚合版本,則系統(tǒng)應(yīng)該引發(fā)異常。
因?yàn)槲覄?chuàng)建了一個(gè)新語音,聚合版本等于0,所以如果我將expectedVersion設(shè)置為1,測試應(yīng)該會引發(fā)異常。
測試用例3:具有有效參數(shù)的ChangeTitle應(yīng)應(yīng)用SpeechTitleChangedEvent:
在這里我將測試,如果沒有錯(cuò)誤,則應(yīng)將newTitle應(yīng)用于演講的標(biāo)題。換句話說:Speech.Title = “更新后新標(biāo)題的值”
由于Apply函數(shù)將事件應(yīng)用于聚合,因此語音的標(biāo)題應(yīng)等于SpeechTitleChangedEvent值的標(biāo)題。
ChangeTitle最終實(shí)現(xiàn):
ChangeTitle的最終實(shí)現(xiàn)應(yīng)該是這樣的。
很簡單,我的標(biāo)題不為空或?yàn)榭?#xff0c;應(yīng)用SpeechTitleChangedEvent。apply函數(shù)使用事件SpeechTitleChangedEvent的值設(shè)置演講標(biāo)題。
檢查聚合版本的代碼是在前面的步驟中開發(fā)的(參見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應(yīng)遵循與ChangeTitle相同的場景
處理申請更新
處理更新標(biāo)題
測試用例1:當(dāng)Command為空時(shí)處理更新應(yīng)該引發(fā)ApplicationArgumentNullException :
在這里我將測試如果updateCommand為空,那么系統(tǒng)應(yīng)該引發(fā)異常。
所以我應(yīng)該模擬所有外部依賴項(xiàng):IUnitOfWork、ISpeechRepository和IEventSourcingSubscriber
我將提供一個(gè)空命令并驗(yàn)證是否引發(fā)了ApplicationArgumentNullException。
測試用例2:當(dāng)語音不存在時(shí)處理更新應(yīng)該引發(fā)ApplicationNotFoundException:
這里我將測試如果要更新的語音不存在,那么系統(tǒng)應(yīng)該引發(fā)異常(ApplicationNotFoundException)。
我必須安排我的存儲庫,以便它返回帶有模擬的空語音:
moqEventStoreRepository.Setup(m?=>?m.GetByIdAsync<Domain.SpeechAggregate.Speech>(command.SpeechId)) .Returns(Task.FromResult((Domain.SpeechAggregate.Speech)null));就像這樣。
測試用例3:當(dāng)命令不為空時(shí)處理更新應(yīng)更新語音標(biāo)題:
這里我測試一下,如果命令不為空,并且數(shù)據(jù)庫中存在要更新的語音,則應(yīng)該更新標(biāo)題。
驗(yàn)證語音標(biāo)題是否被修改的一種方法是在將其發(fā)送到存儲庫之前檢查它的值,它應(yīng)該等于新標(biāo)題的值:
moqSpeechRepository.Verify(m?=> m.UpdateAsync(It.Is<Domain.SpeechAggregate.Speech>(n?=> n.Title.Value.Equals(command.Title) )),Times.Once);測試用例4:當(dāng)預(yù)期版本不等于聚合版本時(shí)處理更新應(yīng)該引發(fā)ConcurrencyException:
在這里我將測試如果預(yù)期版本不等于聚合版本,那么系統(tǒng)應(yīng)該引發(fā)異常。
聚合等于零,因?yàn)槲覍?shí)例化了一個(gè)新的語音,然后如果expectedversion不等于零,則系統(tǒng)應(yīng)該引發(fā)ConcurrencyException。
處理倉儲更新
處理更新
測試用例1:當(dāng)Speech為空時(shí)處理更新應(yīng)該引發(fā)RepositoryArgumentNullException :
測試用例2:當(dāng)語音不存在時(shí)處理更新應(yīng)該引發(fā)NotFoundRepositoryException
測試用例3:當(dāng)語音有效且存在時(shí)處理更新應(yīng)執(zhí)行更新
以及最終的實(shí)現(xiàn)
處理PRESENTATION的更新
處理更新
測試用例1:當(dāng)ModelState無效時(shí)更新語音應(yīng)返回BadRequest:
測試用例2:發(fā)生異常時(shí)的UpdateSpeech應(yīng)引發(fā)InternalServerError
同上注冊語音(ExceptionMiddleware)
測試用例3:當(dāng)ModelState有效且沒有錯(cuò)誤時(shí)更新語音應(yīng)該返回Ok
以及最終的實(shí)現(xiàn)
用POSTMAN測試
按F5并啟動(dòng)postman和sql server。
讓我們啟動(dòng)sql server看看發(fā)生了什么 讓我們運(yùn)行一個(gè)select查詢,你可以看到[dbo].[Speech]和[dbo].[EventStore]這兩個(gè)表是空的。
讓我們啟動(dòng)postman并運(yùn)行一個(gè)post請求來創(chuàng)建一個(gè)演講:http://localhost:62694/api/speech
postman腳本在這里:LogCorner.EduSync.Command\src\Postman\BLOG.postman_collection.json
現(xiàn)在我應(yīng)該有一個(gè)新創(chuàng)建的演講和一個(gè)事件LogCorner.EduSync.Speech.Domain.Events.SpeechCreatedEvent, LogCorner.EduSync.Speech.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 請注意,版本等于 0。對于每個(gè)新語音,版本應(yīng)為零。
如果我檢查有效載荷,我必須看到我的事件
{ “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” }為了更新演講的標(biāo)題,我運(yùn)行以下請求 http://localhost:62694/api/speech 這是一個(gè)PUT請求。
我拿到了新建語音的標(biāo)識符CF17D255-9991-4B7B-B08E-F65B54AA9335 讓我們從sql復(fù)制并將其粘貼到請求正文中。
好的,現(xiàn)在我可以運(yùn)行put查詢
回到sql server驗(yàn)證結(jié)果
SELECT * FROM [dbo].[Speech]
SELECT * FROM [dbo].[EventStore]
我應(yīng)該看到更新的標(biāo)題和一個(gè)新事件LogCorner.EduSync.Speech.Domain.Events.SpeechTitleChangedEvent。
版本應(yīng)為 1,有效負(fù)載應(yīng)為更新事件
{ “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
總結(jié)
以上是生活随笔為你收集整理的基于事件驱动架构构建微服务第9部分:处理更新的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 使用Timer控件设置时间间隔
- 下一篇: Github CodeSpaces 使用