├── .gitattributes ├── .gitignore ├── DevelopmentGuide.md ├── EventDriven.ReferenceArchitecture.sln ├── LICENSE ├── ReadMe.md ├── global.json ├── images ├── eda-logo.jpeg ├── event-driven-ref-arch.drawio └── event-driven-ref-arch.png ├── reference-architecture ├── Common │ ├── Behaviors │ │ └── LoggingBehavior.cs │ ├── Common.csproj │ └── Integration │ │ ├── Events │ │ └── CustomerAddressUpdated.cs │ │ └── Models │ │ └── Address.cs ├── CustomerService │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── Configuration │ │ └── CustomerDatabaseSettings.cs │ ├── Controllers │ │ ├── CustomerCommandController.cs │ │ └── CustomerQueryController.cs │ ├── CustomerService.csproj │ ├── DTO │ │ ├── Read │ │ │ └── CustomerView.cs │ │ └── Write │ │ │ ├── Address.cs │ │ │ └── Customer.cs │ ├── Domain │ │ └── CustomerAggregate │ │ │ ├── Address.cs │ │ │ ├── CommandHandlers │ │ │ ├── CreateCustomerHandler.cs │ │ │ ├── RemoveCustomerHandler.cs │ │ │ └── UpdateCustomerHandler.cs │ │ │ ├── Commands │ │ │ ├── CreateCustomer.cs │ │ │ ├── RemoveCustomer.cs │ │ │ └── UpdateCustomer.cs │ │ │ ├── Customer.cs │ │ │ ├── Events │ │ │ ├── CustomerCreated.cs │ │ │ ├── CustomerRemoved.cs │ │ │ └── CustomerUpdated.cs │ │ │ ├── Queries │ │ │ ├── GetCustomer.cs │ │ │ └── GetCustomers.cs │ │ │ └── QueryHandlers │ │ │ ├── GetCustomerHandler.cs │ │ │ └── GetCustomersHandler.cs │ ├── Mapping │ │ └── AutoMapperProfile.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Repositories │ │ ├── CustomerRepository.cs │ │ └── ICustomerRepository.cs │ ├── appsettings.Development.json │ ├── appsettings.Specs.json │ └── appsettings.json ├── OrderService │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── Configuration │ │ └── OrderDatabaseSettings.cs │ ├── Controllers │ │ ├── OrderCommandController.cs │ │ └── OrderQueryController.cs │ ├── DTO │ │ ├── Read │ │ │ ├── OrderState.cs │ │ │ └── OrderView.cs │ │ └── Write │ │ │ ├── Address.cs │ │ │ ├── Order.cs │ │ │ ├── OrderItem.cs │ │ │ └── OrderState.cs │ ├── Domain │ │ └── OrderAggregate │ │ │ ├── Address.cs │ │ │ ├── CommandHandlers │ │ │ ├── CancelOrderHandler.cs │ │ │ ├── CreateOrderHandler.cs │ │ │ ├── RemoveOrderHandler.cs │ │ │ ├── ShipOrderHandler.cs │ │ │ └── UpdateOrderHandler.cs │ │ │ ├── Commands │ │ │ ├── CancelOrder.cs │ │ │ ├── CreateOrder.cs │ │ │ ├── RemoveOrder.cs │ │ │ ├── ShipOrder.cs │ │ │ └── UpdateOrder.cs │ │ │ ├── Events │ │ │ ├── OrderCancelled.cs │ │ │ ├── OrderCreated.cs │ │ │ ├── OrderRemoved.cs │ │ │ ├── OrderShipped.cs │ │ │ └── OrderUpdated.cs │ │ │ ├── Order.cs │ │ │ ├── OrderItem.cs │ │ │ ├── OrderState.cs │ │ │ ├── Queries │ │ │ ├── GetOrder.cs │ │ │ ├── GetOrders.cs │ │ │ └── GetOrdersByCustomer.cs │ │ │ └── QueryHandlers │ │ │ ├── GetOrderHandler.cs │ │ │ ├── GetOrdersByCustomerHandler.cs │ │ │ └── GetOrdersHandler.cs │ ├── Integration │ │ └── EventHandlers │ │ │ └── CustomerAddressUpdatedEventHandler.cs │ ├── Mapping │ │ └── AutoMapperProfile.cs │ ├── OrderService.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Repositories │ │ ├── IOrderRepository.cs │ │ └── OrderRepository.cs │ ├── appsettings.Development.json │ ├── appsettings.Specs.json │ └── appsettings.json ├── ReferenceArchitecture.AppHost │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ReferenceArchitecture.AppHost.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── ReferenceArchitecture.ServiceDefaults │ ├── Extensions.cs │ └── ReferenceArchitecture.ServiceDefaults.csproj ├── dapr │ ├── components │ │ ├── pubsub.yaml │ │ ├── statestore-mongodb.yaml │ │ └── statestore.yaml │ └── standby │ │ └── pubsub-snssqs.yaml └── json │ ├── customers.json │ └── orders.json └── test ├── CustomerService.Tests ├── Controllers │ ├── CustomerCommandControllerTests.cs │ └── CustomerQueryControllerTests.cs ├── CustomerService.Tests.csproj ├── Domain │ └── CustomerAggregate │ │ ├── CommandHandlers │ │ ├── CreateCustomerHandlerTests.cs │ │ ├── RemoveCustomerHandlerTests.cs │ │ └── UpdateCustomerHandlerTests.cs │ │ ├── CustomerTests.cs │ │ └── QueryHandlers │ │ ├── GetCustomerHandlerTests.cs │ │ └── GetCustomersHandlerTests.cs ├── Fakes │ └── Customers.cs └── Helpers │ ├── CustomerComparer.cs │ └── MappingHelper.cs ├── EventDriven.ReferenceArchitecture.Specs ├── Configuration │ └── ReferenceArchSpecsSettings.cs ├── EventDriven.ReferenceArchitecture.Specs.csproj ├── Features │ ├── CustomerService.feature │ ├── CustomerService.feature.cs │ ├── OrderService.feature │ ├── OrderService.feature.cs │ ├── PublishSubscribe.feature │ └── PublishSubscribe.feature.cs ├── Helpers │ ├── CustomerReadDtoComparer.cs │ ├── CustomerWriteDtoAddressComparer.cs │ ├── CustomerWriteDtoComparer.cs │ ├── JsonHelper.cs │ ├── MappingHelper.cs │ ├── OrderReadDtoComparer.cs │ ├── OrderWriteDtoAddressComparer.cs │ └── PutRequest.cs ├── Hooks │ └── Hook.cs ├── ReadMe.md ├── Repositories │ └── JsonFilesRepository.cs ├── Steps │ └── StepDefinitions.cs ├── appsettings.json ├── json │ ├── cancelled-order.json │ ├── customer-pubsub.json │ ├── customer-view.json │ ├── customer.json │ ├── customers-view.json │ ├── customers.json │ ├── order-to-cancel.json │ ├── order-to-ship.json │ ├── order-view.json │ ├── order.json │ ├── orders-pubsub.json │ ├── orders-view.json │ ├── orders.json │ ├── shipped-order.json │ ├── updated-address-pubsub.json │ ├── updated-customer-pubsub.json │ ├── updated-customer.json │ └── updated-order.json ├── specflow.json └── xunit.runner.json └── OrderService.Tests ├── Controllers ├── OrderCommandControllerTests.cs └── OrderQueryControllerTests.cs ├── Domain └── OrderAggregate │ ├── CommandHandlers │ ├── CancelOrderHandlerTests.cs │ ├── CreateOrderHandlerTests.cs │ ├── RemoveOrderHandlerTests.cs │ ├── ShipOrderHandlerTests.cs │ └── UpdateOrderHandlerTests.cs │ ├── OrderTests.cs │ └── QueryHandlers │ ├── GetOrderHandlerTests.cs │ ├── GetOrdersByCustomerHandlerTests.cs │ └── GetOrdersHandlerTests.cs ├── Fakes └── Orders.cs ├── Helpers └── MappingHelper.cs ├── Integration └── EventHandlers │ └── CustomerAddressUpdatedEventHandlerTests.cs └── OrderService.Tests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | .idea 365 | .DS_Store 366 | **/.tye 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Event Driven Architectures for .NET 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.300", 4 | "rollForward": "latestPatch", 5 | "allowPrerelease": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /images/eda-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-dotnet/EventDriven.ReferenceArchitecture/157ea8ad1e602d7a9c7fc5c4f27fe8a2fc0907f0/images/eda-logo.jpeg -------------------------------------------------------------------------------- /images/event-driven-ref-arch.drawio: -------------------------------------------------------------------------------- 1 | 7VzbdtsoFP0ar9U+pEtXXx6dOG1mTWaaxr1M+4YlIjPFQoNQbOfrByR0BTt2Y9ly25dEOoAue7MPhwNyz75arN5REM3/Ij7EPcvwVz170rOsUX/E/wrDOjO4IzszBBT5mcksDVP0BKXRkNYE+TCuVWSEYIaiutEjYQg9VrMBSsmyXu2B4PpdIxBAxTD1AFatX5DP5pl16Bql/QaiYJ7f2TRkyQLklaUhngOfLCsm+7pnX1FCWHa0WF1BLLDLccnavd1QWjwYhSHbpcG3h/v5zCOOv/rz7tPq4+cb+M29sCU9MVvnbwx9DoA8DUnI/11SkoQ+FNcx+BmhbE4CEgJ8S0jEjSY3/gsZW0v6QMIIN83ZAstSuELsH9H8jeXK06+VoslKXjo9WecnIaPrrJWZNROGr9XSsmF6Vmt5BylaQAapND6QkMkHNB1+ruInIY1JQj24BTTTlR0R0ACybejKigLSyi0kP+8g4c9H17wChRgw9Fjvc0B23aCoV7LLDyTBe5CdP/cjwIm81RVZLEDoc+MN/4c5WM3uUOd+OUcMTiOQ4rPkiq/z3MT4AWF8RTChZV96hJQhrq8xRkHIzUx0oc1kiOpwtRU8WTqQjkU6GrsvdbcsZWvmtnlVsgOjLbwVuK8fxftZxoRyskN+cA8fIH9nDqdljKkn0PVYQqFCAweB1bGOGSXfYQNdDeBAAu3xWwsxKAwskO+L22jJrdNf5XeYn8uH1DikvTm07EGNREfl0DE1HDptUai6x5f7wtKt7eXUqtj3t0nmWbc0PLRXkk3vCEp79ypnqq7IYkzPL5E5WtmqQVTxGD/OnaXITyGTj8mROEwWeOwxUlXHLZhBfEdixBARKpkRxshiowOrEEwShlHIlZlHJDrlHEAsjlXH19SoxdaIpd+avzuPYGK/WGLnsOFZ2XHVHkV3plvvF659XN1tizK4KPiwhY8RaLQQUlg7hhStDUfFXKJlhZViGbxILKVUd1fqRt6qcfko/JBYN9dB8P2zO7ocLG+e7o2LrgnRbgyAbrNftCzEviLEDwlMX+y8ZGjZXdPhQHVxSczDA4GnMYX0EXnnEMKLMHJOKHriNoCrFQ4a09ejwKFKnqvhzm6NvKEmLuxjQU3CDwJxcH89/Zgb+T0Ke88e85bvuHex+L0NHh7mR4k8mEAMmcp9B1U1eJ6XQmhHEVU+PdAQE0cgrAHa/y8RebNLL4NFsEKD2St+BSPjoXL0WhwKyAwB7MUDWCC8ztrwC4FFln+wbQH1HOJHKKSllNQvEqf0iEuYTrRqlGVPKQpDQhcA14uXEktR7mTPmRbyfsPFfcFf1UNhoG0vPMgF4h0plO2Nyr3TQkZBGD/wVnn7EBYVloT69ctXm8+A9z1Iu+lFA1TLGRZgWs6oPHYr0PoojjCQsKJQTIDK18YEsOoD5ezlsrpOY8NPsXCfGeEzmpd9gTPhU+9E6yuM0vxJIcysX0htbvC3e4ptd197AA3a5rAmwiJ7XBFhIbiad2xNhGrSqhzamtxMPr5X41GMURRvGov2Af4Q+BoNJ2e4Cr5DDbxua/DaG33cLEe1iTe/0azs5E0OxkFAYQDEmGNchwyxtVpHtdxR4sE4TkPBdG4Wa64cRVhgOSELgEK1PE1mxi12gGNmj4f1juJohGjqhGg2k1qH6ypDFdpWsyk/lEzp1LqMnPA8PwGU8XtX1mXUiVplOnEPI5GFJPJGbUaVbbjgRpjpapZltGFme1nKrZO3CWBgBmI1gs9Txd4aiwiM2s8jPsvouZ0VhiLGep/liPN5oKTFPR1NpluPRPquxgHq5mlFyHL4ZJcmFPl5liud7q1XivZb4TzL1S7r4M5+w3JX/8TLXfaR44XzWn2xzeN0A/fEqy+Ws8VrnlfeV3GRJ8/7Wu5xNHae6y9Wx6Q4aKy/OM2Ysm0p/iwLMP3OLcBYahD/Sybxmy7y9Fl8S11eeU/9s8weul3LHlqjjb2+yBDWwP6dOuwdYSbXvdxhHnP+zh3umju0d55HWIcOXl7GtJokkR7g3BOHzaHt9JnDnHoN1r9q2tDtXtrQVpfYcpZ+78zZuoH49FtznGPnsKrjVjFU7TVuFfP1E41bznmOW45zDhHKYDCqcn1hvDHcQ/ANQ38sPk4s35Rb3iKcKz3mdLK8hodBHCMvN8tq5pG6zcH3yr7MuWuym+Xi3dj3qZyazEEY8I6jn3n0jrd+dIzIaLDrpL+1yMgxFSA7qGaBa0PN5uDE7nvnT0k7pkPNRx576fAPLoeAgvTjKsuQqjxDLXZOiraadFZyRBMQ0RJ14zKJ9QmjjXyY58LHcKTyMTguH2qm+h76SJM0e3VL+MThtQJ7PoVkHGP4RMTdLqOKRyrsFTf1LDtoBfNfWzghW1ZjzXw00Ewfdd8wtrYHwlbT1+MvU5Ws6d/ceDn9oCnKeOTXIEJkV5gkvkpqmeosMgSiYmd11UxyjnQTfV0yvL0kp6OmY4qozshpQTkrUTLDKJ7DqqtDpaszUG1AgtoBacc8QVtJga2hxh5bvRrLoiNTpZIPWm9clUyrNS7VeKJI1hRc1nf7N/iNk1nsUTTbRPCGbwYMHmOeDfnVBJBxgJ7QXODS9oSBRtTt9QM1eiHqJK2b+bpDeFnLqhMyUlccdStJh+Bj2ye1mh0MXdiAqQCrgX9z7NHc1LDrBsxhW2Cr49lPA7ayg6RFsPlp+ata2V6e8qfJ7Ov/AQ== -------------------------------------------------------------------------------- /images/event-driven-ref-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-dotnet/EventDriven.ReferenceArchitecture/157ea8ad1e602d7a9c7fc5c4f27fe8a2fc0907f0/images/event-driven-ref-arch.png -------------------------------------------------------------------------------- /reference-architecture/Common/Behaviors/LoggingBehavior.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Behaviors; 2 | using EventDriven.CQRS.Extensions; 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Common.Behaviors; 7 | 8 | public class LoggingBehavior : IBehavior 9 | where TRequest : IRequest 10 | { 11 | private readonly ILogger> _logger; 12 | 13 | public LoggingBehavior( 14 | ILogger> logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public async Task Handle(TRequest request, CancellationToken cancellationToken, 20 | RequestHandlerDelegate next) 21 | { 22 | string requestType = string.Empty; 23 | if (typeof(TRequest).IsCommandType()) 24 | requestType = "command"; 25 | else if (typeof(TRequest).IsQueryType()) 26 | requestType = "query"; 27 | _logger.LogInformation("----- Handling {RequestType} '{CommandName}'. Request: {@Request}", 28 | requestType, request.GetGenericTypeName(), request); 29 | var response = await next(); 30 | _logger.LogInformation("----- Handled {RequestType} '{CommandName}'. Response: {@Response}", 31 | requestType, request.GetGenericTypeName(), response); 32 | return response; 33 | } 34 | } -------------------------------------------------------------------------------- /reference-architecture/Common/Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /reference-architecture/Common/Integration/Events/CustomerAddressUpdated.cs: -------------------------------------------------------------------------------- 1 | using Common.Integration.Models; 2 | using EventDriven.EventBus.Abstractions; 3 | 4 | namespace Common.Integration.Events 5 | { 6 | public record CustomerAddressUpdated(Guid CustomerId, Address ShippingAddress) : IntegrationEvent; 7 | } -------------------------------------------------------------------------------- /reference-architecture/Common/Integration/Models/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Integration.Models 2 | { 3 | public record Address(string Street, string City, string State, string Country, string PostalCode); 4 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (web)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/bin/Debug/net5.0/CustomerService.dll", 10 | "args": [], 11 | "cwd": "${workspaceFolder}", 12 | "stopAtEntry": false, 13 | "serverReadyAction": { 14 | "action": "openExternally", 15 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 16 | }, 17 | "env": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | }, 20 | "sourceFileMap": { 21 | "/Views": "${workspaceFolder}/Views" 22 | } 23 | }, 24 | { 25 | "name": ".NET Core Attach", 26 | "type": "coreclr", 27 | "request": "attach", 28 | "processId": "${command:pickProcess}" 29 | }, 30 | { 31 | "name": ".NET Core Launch (web) with Dapr", 32 | "type": "coreclr", 33 | "request": "launch", 34 | "preLaunchTask": "daprd-debug", 35 | "program": "${workspaceFolder}/bin/Debug/net5.0/CustomerService.dll", 36 | "args": [], 37 | "cwd": "${workspaceFolder}", 38 | "stopAtEntry": false, 39 | "serverReadyAction": { 40 | "action": "openExternally", 41 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 42 | }, 43 | "env": { 44 | "ASPNETCORE_ENVIRONMENT": "Development" 45 | }, 46 | "sourceFileMap": { 47 | "/Views": "${workspaceFolder}/Views" 48 | }, 49 | "postDebugTask": "daprd-down" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/CustomerService.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/CustomerService.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/CustomerService.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | }, 41 | { 42 | "appId": "customer-service", 43 | "appPort": 5000, 44 | "label": "daprd-debug", 45 | "type": "daprd", 46 | "dependsOn": "build" 47 | }, 48 | { 49 | "appId": "customer-service", 50 | "label": "daprd-down", 51 | "type": "daprd-down" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Configuration/CustomerDatabaseSettings.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DependencyInjection.URF.Mongo; 2 | 3 | namespace CustomerService.Configuration; 4 | 5 | public class CustomerDatabaseSettings : IMongoDbSettings 6 | { 7 | public string ConnectionString { get; set; } = null!; 8 | public string DatabaseName { get; set; } = null!; 9 | public string CollectionName { get; set; } = null!; 10 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Controllers/CustomerCommandController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CustomerService.Domain.CustomerAggregate; 3 | using CustomerService.Domain.CustomerAggregate.Commands; 4 | using EventDriven.CQRS.Abstractions.Commands; 5 | using EventDriven.CQRS.Extensions; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace CustomerService.Controllers; 9 | 10 | [Route("api/customer")] 11 | [ApiController] 12 | public class CustomerCommandController : ControllerBase 13 | { 14 | private readonly ICommandBroker _commandBroker; 15 | private readonly IMapper _mapper; 16 | 17 | public CustomerCommandController( 18 | ICommandBroker commandBroker, 19 | IMapper mapper) 20 | { 21 | _commandBroker = commandBroker; 22 | _mapper = mapper; 23 | } 24 | 25 | // POST api/customer 26 | [HttpPost] 27 | public async Task Create([FromBody] DTO.Write.Customer customerDto) 28 | { 29 | var customerIn = _mapper.Map(customerDto); 30 | var result = await _commandBroker.SendAsync(new CreateCustomer(customerIn)); 31 | 32 | if (result.Outcome != CommandOutcome.Accepted) 33 | return result.ToActionResult(); 34 | var customerOut = _mapper.Map(result.Entity); 35 | return new CreatedResult($"api/customer/{customerOut.Id}", customerOut); 36 | } 37 | 38 | // PUT api/customer 39 | [HttpPut] 40 | public async Task Update([FromBody] DTO.Write.Customer customerDto) 41 | { 42 | var customerIn = _mapper.Map(customerDto); 43 | var result = await _commandBroker.SendAsync(new UpdateCustomer(customerIn)); 44 | 45 | if (result.Outcome != CommandOutcome.Accepted) 46 | return result.ToActionResult(); 47 | var customerOut = _mapper.Map(result.Entity); 48 | return result.ToActionResult(customerOut); 49 | } 50 | 51 | // DELETE api/customer/id 52 | [HttpDelete] 53 | [Route("{id}")] 54 | public async Task Remove([FromRoute] Guid id) 55 | { 56 | var result = await _commandBroker.SendAsync(new RemoveCustomer(id)); 57 | return result.Outcome != CommandOutcome.Accepted 58 | ? result.ToActionResult() 59 | : new NoContentResult(); 60 | } 61 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Controllers/CustomerQueryController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CustomerService.Domain.CustomerAggregate.Queries; 3 | using CustomerService.DTO.Read; 4 | using EventDriven.CQRS.Abstractions.Queries; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace CustomerService.Controllers; 8 | 9 | [Route("api/customer")] 10 | [ApiController] 11 | public class CustomerQueryController : ControllerBase 12 | { 13 | private readonly IQueryBroker _queryBroker; 14 | private readonly IMapper _mapper; 15 | 16 | public CustomerQueryController( 17 | IQueryBroker queryBroker, 18 | IMapper mapper) 19 | { 20 | _queryBroker = queryBroker; 21 | _mapper = mapper; 22 | } 23 | 24 | // GET api/customer 25 | [HttpGet] 26 | public async Task GetCustomers() 27 | { 28 | var customers = await _queryBroker.SendAsync(new GetCustomers()); 29 | var result = _mapper.Map>(customers); 30 | return Ok(result); 31 | } 32 | 33 | // GET api/customer/id 34 | [HttpGet] 35 | [Route("{id:guid}")] 36 | public async Task GetCustomer([FromRoute] Guid id) 37 | { 38 | var customer = await _queryBroker.SendAsync(new GetCustomer(id)); 39 | if (customer == null) return NotFound(); 40 | var result = _mapper.Map(customer); 41 | return Ok(result); 42 | } 43 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/CustomerService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /reference-architecture/CustomerService/DTO/Read/CustomerView.cs: -------------------------------------------------------------------------------- 1 | namespace CustomerService.DTO.Read; 2 | 3 | public class CustomerView 4 | { 5 | public Guid Id { get; set; } 6 | public string FirstName { get; set; } = null!; 7 | public string LastName { get; set; } = null!; 8 | public string Street { get; set; } = null!; 9 | public string City { get; set; } = null!; 10 | public string State { get; set; } = null!; 11 | public string Country { get; set; } = null!; 12 | public string PostalCode { get; set; } = null!; 13 | public string? ETag { get; set; } 14 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/DTO/Write/Address.cs: -------------------------------------------------------------------------------- 1 | namespace CustomerService.DTO.Write; 2 | 3 | public class Address 4 | { 5 | public string Street { get; set; } = null!; 6 | public string City { get; set; } = null!; 7 | public string State { get; set; } = null!; 8 | public string Country { get; set; } = null!; 9 | public string PostalCode { get; set; } = null!; 10 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/DTO/Write/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace CustomerService.DTO.Write; 2 | 3 | public class Customer 4 | { 5 | public Guid Id { get; set; } 6 | public string FirstName { get; set; } = null!; 7 | public string LastName { get; set; } = null!; 8 | public Address ShippingAddress { get; set; } = null!; 9 | public string ETag { get; set; } = null!; 10 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Address.cs: -------------------------------------------------------------------------------- 1 | namespace CustomerService.Domain.CustomerAggregate; 2 | 3 | public record Address(string Street, string City, string State, string Country, string PostalCode); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/CommandHandlers/CreateCustomerHandler.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.Domain.CustomerAggregate.Commands; 2 | using CustomerService.Repositories; 3 | using EventDriven.CQRS.Abstractions.Commands; 4 | 5 | namespace CustomerService.Domain.CustomerAggregate.CommandHandlers; 6 | 7 | public class CreateCustomerHandler : ICommandHandler 8 | { 9 | private readonly ICustomerRepository _repository; 10 | 11 | public CreateCustomerHandler( 12 | ICustomerRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task> Handle(CreateCustomer command, CancellationToken cancellationToken) 18 | { 19 | // Process command 20 | if (command.Entity == null) return new CommandResult(CommandOutcome.InvalidCommand); 21 | var domainEvent = command.Entity.Process(command); 22 | 23 | // Apply events 24 | command.Entity.Apply(domainEvent); 25 | 26 | // Persist entity 27 | var entity = await _repository.AddAsync(command.Entity); 28 | if (entity == null) return new CommandResult(CommandOutcome.InvalidCommand); 29 | return new CommandResult(CommandOutcome.Accepted, entity); 30 | } 31 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/CommandHandlers/RemoveCustomerHandler.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.Domain.CustomerAggregate.Commands; 2 | using CustomerService.Repositories; 3 | using EventDriven.CQRS.Abstractions.Commands; 4 | 5 | namespace CustomerService.Domain.CustomerAggregate.CommandHandlers; 6 | 7 | public class RemoveCustomerHandler : ICommandHandler 8 | { 9 | private readonly ICustomerRepository _repository; 10 | 11 | public RemoveCustomerHandler( 12 | ICustomerRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task Handle(RemoveCustomer command, CancellationToken cancellationToken) 18 | { 19 | var entity = await _repository.GetAsync(command.EntityId); 20 | if (entity != null) 21 | { 22 | // Process command, apply events 23 | var domainEvent = entity.Process(command); 24 | entity.Apply(domainEvent); 25 | } 26 | 27 | // Persist entity 28 | await _repository.RemoveAsync(command.EntityId); 29 | return new CommandResult(CommandOutcome.Accepted); 30 | } 31 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/CommandHandlers/UpdateCustomerHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Common.Integration.Events; 3 | using CustomerService.Domain.CustomerAggregate.Commands; 4 | using CustomerService.Repositories; 5 | using EventDriven.CQRS.Abstractions.Commands; 6 | using EventDriven.DDD.Abstractions.Repositories; 7 | using EventDriven.EventBus.Abstractions; 8 | using Integration = Common.Integration; 9 | 10 | namespace CustomerService.Domain.CustomerAggregate.CommandHandlers; 11 | 12 | public class UpdateCustomerHandler : ICommandHandler 13 | { 14 | private readonly ICustomerRepository _repository; 15 | private readonly IEventBus _eventBus; 16 | private readonly IMapper _mapper; 17 | private readonly ILogger _logger; 18 | 19 | public UpdateCustomerHandler( 20 | ICustomerRepository repository, 21 | IEventBus eventBus, 22 | IMapper mapper, 23 | ILogger logger) 24 | { 25 | _repository = repository; 26 | _eventBus = eventBus; 27 | _mapper = mapper; 28 | _logger = logger; 29 | } 30 | 31 | public async Task> Handle(UpdateCustomer command, CancellationToken cancellationToken) 32 | { 33 | // Process command 34 | if (command.Entity == null) return new CommandResult(CommandOutcome.InvalidCommand); 35 | var domainEvent = command.Entity.Process(command); 36 | 37 | // Apply events 38 | command.Entity.Apply(domainEvent); 39 | 40 | // Compare shipping addresses 41 | var existing = await _repository.GetAsync(command.EntityId); 42 | if (existing == null) return new CommandResult(CommandOutcome.NotHandled); 43 | var addressChanged = command.Entity.ShippingAddress != existing.ShippingAddress; 44 | 45 | try 46 | { 47 | // Persist entity 48 | var entity = await _repository.UpdateAsync(command.Entity); 49 | if (entity == null) return new CommandResult(CommandOutcome.NotFound); 50 | 51 | // Publish events 52 | if (addressChanged) 53 | { 54 | var shippingAddress = _mapper.Map(entity.ShippingAddress); 55 | _logger.LogInformation("----- Publishing event: {EventName}", $"v1.{nameof(CustomerAddressUpdated)}"); 56 | await _eventBus.PublishAsync( 57 | new CustomerAddressUpdated(entity.Id, shippingAddress), 58 | null, "v1"); 59 | } 60 | return new CommandResult(CommandOutcome.Accepted, entity); 61 | } 62 | catch (ConcurrencyException) 63 | { 64 | return new CommandResult(CommandOutcome.Conflict); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Commands/CreateCustomer.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Commands; 4 | 5 | public record CreateCustomer(Customer? Entity) : Command(Entity); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Commands/RemoveCustomer.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Commands; 4 | 5 | public record RemoveCustomer(Guid EntityId) : Command(EntityId); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Commands/UpdateCustomer.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Commands; 4 | 5 | public record UpdateCustomer(Customer? Entity) : Command(Entity); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Customer.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.Domain.CustomerAggregate.Commands; 2 | using CustomerService.Domain.CustomerAggregate.Events; 3 | using EventDriven.CQRS.Abstractions.Commands; 4 | using EventDriven.DDD.Abstractions.Entities; 5 | using EventDriven.DDD.Abstractions.Events; 6 | 7 | namespace CustomerService.Domain.CustomerAggregate; 8 | 9 | public class Customer : 10 | Entity, 11 | ICommandProcessor, 12 | IEventApplier, 13 | ICommandProcessor, 14 | IEventApplier, 15 | ICommandProcessor, 16 | IEventApplier 17 | { 18 | public string FirstName { get; set; } = null!; 19 | public string LastName { get; set; } = null!; 20 | public Address ShippingAddress { get; set; } = null!; 21 | 22 | public CustomerCreated Process(CreateCustomer command) 23 | // To process command, return one or more domain events 24 | => new(command.Entity); 25 | 26 | public void Apply(CustomerCreated domainEvent) => 27 | // Set Id 28 | Id = domainEvent.EntityId != default ? domainEvent.EntityId : Guid.NewGuid(); 29 | 30 | public CustomerUpdated Process(UpdateCustomer command) => 31 | // To process command, return a domain event 32 | new(command.Entity); 33 | 34 | public void Apply(CustomerUpdated domainEvent) 35 | { 36 | // Set ETag 37 | if (domainEvent.EntityETag != null) ETag = domainEvent.EntityETag; 38 | } 39 | 40 | public CustomerRemoved Process(RemoveCustomer command) => 41 | // To process command, return a domain event 42 | new(command.EntityId); 43 | 44 | public void Apply(CustomerRemoved domainEvent) 45 | { 46 | // Could mutate state here to implement a soft delete 47 | } 48 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Events/CustomerCreated.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Events; 4 | 5 | public record CustomerCreated(Customer? Entity) : DomainEvent(Entity); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Events/CustomerRemoved.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Events; 4 | 5 | public record CustomerRemoved(Guid EntityId) : DomainEvent(EntityId); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Events/CustomerUpdated.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Events; 4 | 5 | public record CustomerUpdated(Customer? Entity) : DomainEvent(Entity); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Queries/GetCustomer.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Queries; 4 | 5 | public record GetCustomer(Guid Id) : Query; -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/Queries/GetCustomers.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | 3 | namespace CustomerService.Domain.CustomerAggregate.Queries; 4 | 5 | public record GetCustomers : Query>; -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/QueryHandlers/GetCustomerHandler.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.Domain.CustomerAggregate.Queries; 2 | using CustomerService.Repositories; 3 | using EventDriven.CQRS.Abstractions.Queries; 4 | 5 | namespace CustomerService.Domain.CustomerAggregate.QueryHandlers; 6 | 7 | public class GetCustomerHandler : IQueryHandler 8 | { 9 | private readonly ICustomerRepository _repository; 10 | 11 | public GetCustomerHandler( 12 | ICustomerRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task Handle(GetCustomer query, CancellationToken cancellationToken) 18 | { 19 | var result = await _repository.GetAsync(query.Id); 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Domain/CustomerAggregate/QueryHandlers/GetCustomersHandler.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.Domain.CustomerAggregate.Queries; 2 | using CustomerService.Repositories; 3 | using EventDriven.CQRS.Abstractions.Queries; 4 | 5 | namespace CustomerService.Domain.CustomerAggregate.QueryHandlers; 6 | 7 | public class GetCustomersHandler : IQueryHandler> 8 | { 9 | private readonly ICustomerRepository _repository; 10 | 11 | public GetCustomersHandler( 12 | ICustomerRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task> Handle(GetCustomers query, CancellationToken cancellationToken) 18 | { 19 | var result = await _repository.GetAsync(); 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Mapping/AutoMapperProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CustomerService.Domain.CustomerAggregate; 3 | using Integration = Common.Integration.Models; 4 | 5 | namespace CustomerService.Mapping; 6 | 7 | public class AutoMapperProfile : Profile 8 | { 9 | public AutoMapperProfile() 10 | { 11 | CreateMap(); 12 | CreateMap().ReverseMap(); 13 | CreateMap(); 14 | CreateMap().ReverseMap(); 15 | 16 | CreateMap().IncludeMembers(c => c.ShippingAddress); 17 | CreateMap().IncludeMembers(c => c.ShippingAddress).ReverseMap(); 18 | CreateMap(); 19 | CreateMap().ReverseMap(); 20 | 21 | CreateMap(); 22 | CreateMap().ReverseMap(); 23 | } 24 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Behaviors; 2 | using CustomerService.Configuration; 3 | using CustomerService.Domain.CustomerAggregate; 4 | using CustomerService.Repositories; 5 | using EventDriven.CQRS.Abstractions.DependencyInjection; 6 | using EventDriven.DependencyInjection.URF.Mongo; 7 | using MediatR; 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | 11 | // Add Aspire service defaults 12 | builder.AddServiceDefaults(); 13 | 14 | // Add services to the container. 15 | builder.Services.AddControllers(); 16 | builder.Services.AddEndpointsApiExplorer(); 17 | builder.Services.AddSwaggerGen(); 18 | builder.Services.AddProblemDetails(); 19 | 20 | // Add automapper 21 | builder.Services.AddAutoMapper(typeof(Program)); 22 | 23 | // Add command and query handlers 24 | builder.Services.AddHandlers(typeof(Program)); 25 | 26 | // Add behaviors 27 | builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); 28 | 29 | // Add database settings 30 | builder.Services.AddSingleton(); 31 | builder.Services.AddMongoDbSettings(builder.Configuration); 32 | 33 | // Add Dapr event bus 34 | builder.Services.AddDaprEventBus(builder.Configuration); 35 | 36 | var app = builder.Build(); 37 | 38 | // Configure the HTTP request pipeline. 39 | if (app.Environment.IsDevelopment()) 40 | { 41 | app.UseSwagger(); 42 | app.UseSwaggerUI(); 43 | } 44 | 45 | app.UseRouting(); 46 | app.UseAuthorization(); 47 | app.MapControllers(); 48 | 49 | app.Run(); -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:5656;http://localhost:5657", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "specs": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "swagger", 19 | "applicationUrl": "https://localhost:5656;http://localhost:5657", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Specs" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Repositories/CustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using CustomerService.Domain.CustomerAggregate; 3 | using EventDriven.DDD.Abstractions.Repositories; 4 | using MongoDB.Driver; 5 | using URF.Core.Mongo; 6 | 7 | namespace CustomerService.Repositories; 8 | 9 | [ExcludeFromCodeCoverage] 10 | public class CustomerRepository : DocumentRepository, ICustomerRepository 11 | { 12 | public CustomerRepository(IMongoCollection collection) : base(collection) 13 | { 14 | } 15 | 16 | public async Task> GetAsync() => 17 | await FindManyAsync(); 18 | 19 | public async Task GetAsync(Guid id) => 20 | await FindOneAsync(e => e.Id == id); 21 | 22 | public async Task AddAsync(Customer entity) 23 | { 24 | var existing = await FindOneAsync(e => e.Id == entity.Id); 25 | if (existing != null) return null; 26 | if (string.IsNullOrWhiteSpace(entity.ETag)) 27 | entity.ETag = Guid.NewGuid().ToString(); 28 | return await InsertOneAsync(entity); 29 | } 30 | 31 | public async Task UpdateAsync(Customer entity) 32 | { 33 | var existing = await GetAsync(entity.Id); 34 | if (existing == null) return null; 35 | if (string.Compare(entity.ETag, existing.ETag, StringComparison.OrdinalIgnoreCase) != 0 ) 36 | throw new ConcurrencyException(); 37 | entity.ETag = Guid.NewGuid().ToString(); 38 | return await FindOneAndReplaceAsync(e => e.Id == entity.Id, entity); 39 | } 40 | 41 | public async Task RemoveAsync(Guid id) => 42 | await DeleteOneAsync(e => e.Id == id); 43 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/Repositories/ICustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.Domain.CustomerAggregate; 2 | 3 | namespace CustomerService.Repositories; 4 | 5 | public interface ICustomerRepository 6 | { 7 | Task> GetAsync(); 8 | Task GetAsync(Guid id); 9 | Task AddAsync(Customer entity); 10 | Task UpdateAsync(Customer entity); 11 | Task RemoveAsync(Guid id); 12 | } -------------------------------------------------------------------------------- /reference-architecture/CustomerService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /reference-architecture/CustomerService/appsettings.Specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "CustomerDatabaseSettings": { 10 | "ConnectionString": "mongodb://localhost:27017", 11 | "DatabaseName": "CustomersTestDb", 12 | "CollectionName": "Customers" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /reference-architecture/CustomerService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "CustomerDatabaseSettings": { 11 | "ConnectionString": "mongodb://localhost:27017", 12 | "DatabaseName": "CustomersDb", 13 | "CollectionName": "Customers" 14 | }, 15 | "DaprEventBusOptions": { 16 | "PubSubName": "pubsub" 17 | }, 18 | "DaprEventBusSchemaOptions": { 19 | "UseSchemaRegistry": true, 20 | "SchemaValidatorType": "Json", 21 | "SchemaRegistryType": "Mongo", 22 | "AddSchemaOnPublish": true, 23 | "MongoStateStoreOptions": { 24 | "ConnectionString": "mongodb://localhost:27017", 25 | "DatabaseName": "schema-registry", 26 | "SchemasCollectionName": "schemas" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /reference-architecture/OrderService/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (web)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/bin/Debug/net5.0/OrderService.dll", 10 | "args": [], 11 | "cwd": "${workspaceFolder}", 12 | "stopAtEntry": false, 13 | "serverReadyAction": { 14 | "action": "openExternally", 15 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 16 | }, 17 | "env": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | }, 20 | "sourceFileMap": { 21 | "/Views": "${workspaceFolder}/Views" 22 | } 23 | }, 24 | { 25 | "name": ".NET Core Attach", 26 | "type": "coreclr", 27 | "request": "attach", 28 | "processId": "${command:pickProcess}" 29 | }, 30 | { 31 | "name": ".NET Core Launch (web) with Dapr", 32 | "type": "coreclr", 33 | "request": "launch", 34 | "preLaunchTask": "daprd-debug", 35 | "program": "${workspaceFolder}/bin/Debug/net5.0/OrderService.dll", 36 | "args": [], 37 | "cwd": "${workspaceFolder}", 38 | "stopAtEntry": false, 39 | "serverReadyAction": { 40 | "action": "openExternally", 41 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 42 | }, 43 | "env": { 44 | "ASPNETCORE_ENVIRONMENT": "Development" 45 | }, 46 | "sourceFileMap": { 47 | "/Views": "${workspaceFolder}/Views" 48 | }, 49 | "postDebugTask": "daprd-down" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/OrderService.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/OrderService.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/OrderService.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | }, 41 | { 42 | "appId": "order-service", 43 | "appPort": 5050, 44 | "label": "daprd-debug", 45 | "type": "daprd", 46 | "dependsOn": "build" 47 | }, 48 | { 49 | "appId": "order-service", 50 | "label": "daprd-down", 51 | "type": "daprd-down" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Configuration/OrderDatabaseSettings.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DependencyInjection.URF.Mongo; 2 | 3 | namespace OrderService.Configuration; 4 | 5 | public class OrderDatabaseSettings : IMongoDbSettings 6 | { 7 | public string ConnectionString { get; set; } = null!; 8 | public string DatabaseName { get; set; } = null!; 9 | public string CollectionName { get; set; } = null!; 10 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Controllers/OrderCommandController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EventDriven.CQRS.Abstractions.Commands; 3 | using EventDriven.CQRS.Extensions; 4 | using Microsoft.AspNetCore.Mvc; 5 | using OrderService.Domain.OrderAggregate; 6 | using OrderService.Domain.OrderAggregate.Commands; 7 | 8 | namespace OrderService.Controllers; 9 | 10 | [Route("api/order")] 11 | [ApiController] 12 | public class OrderCommandController : ControllerBase 13 | { 14 | private readonly ICommandBroker _commandBroker; 15 | private readonly IMapper _mapper; 16 | 17 | public OrderCommandController( 18 | ICommandBroker commandBroker, 19 | IMapper mapper) 20 | { 21 | _commandBroker = commandBroker; 22 | _mapper = mapper; 23 | } 24 | 25 | // POST api/order 26 | [HttpPost] 27 | public async Task Create([FromBody] DTO.Write.Order orderDto) 28 | { 29 | var orderIn = _mapper.Map(orderDto); 30 | var result = await _commandBroker.SendAsync(new CreateOrder(orderIn)); 31 | 32 | if (result.Outcome != CommandOutcome.Accepted) 33 | return result.ToActionResult(); 34 | var orderOut = _mapper.Map(result.Entity); 35 | return new CreatedResult($"api/order/{orderOut.Id}", orderOut); 36 | } 37 | 38 | // PUT api/order 39 | [HttpPut] 40 | public async Task Update([FromBody] DTO.Write.Order orderDto) 41 | { 42 | var orderIn = _mapper.Map(orderDto); 43 | var result = await _commandBroker.SendAsync(new UpdateOrder(orderIn)); 44 | 45 | if (result.Outcome != CommandOutcome.Accepted) 46 | return result.ToActionResult(); 47 | var orderOut = _mapper.Map(result.Entity); 48 | return result.ToActionResult(orderOut); 49 | } 50 | 51 | // DELETE api/order 52 | [HttpDelete] 53 | [Route("{id:guid}")] 54 | public async Task Remove([FromRoute] Guid id) 55 | { 56 | var result = await _commandBroker.SendAsync(new RemoveOrder(id)); 57 | return result.Outcome != CommandOutcome.Accepted 58 | ? result.ToActionResult() 59 | : new NoContentResult(); 60 | } 61 | 62 | // PUT api/order/ship 63 | [HttpPut] 64 | [Route("ship/{id:guid}/{etag}")] 65 | public async Task Ship([FromRoute] Guid id) 66 | { 67 | var result = await _commandBroker.SendAsync(new ShipOrder(id)); 68 | 69 | if (result.Outcome != CommandOutcome.Accepted) 70 | return result.ToActionResult(); 71 | var orderOut = _mapper.Map(result.Entity); 72 | return result.ToActionResult(orderOut); 73 | } 74 | 75 | // PUT api/order/cancel 76 | [HttpPut] 77 | [Route("cancel/{id:guid}/{etag}")] 78 | public async Task Cancel([FromRoute] Guid id) 79 | { 80 | var result = await _commandBroker.SendAsync(new CancelOrder(id)); 81 | 82 | if (result.Outcome != CommandOutcome.Accepted) 83 | return result.ToActionResult(); 84 | var orderOut = _mapper.Map(result.Entity); 85 | return result.ToActionResult(orderOut); 86 | } 87 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Controllers/OrderQueryController.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | using Microsoft.AspNetCore.Mvc; 3 | using OrderService.Domain.OrderAggregate; 4 | using OrderService.Domain.OrderAggregate.Queries; 5 | using OrderService.DTO.Read; 6 | using OrderState = OrderService.DTO.Read.OrderState; 7 | 8 | namespace OrderService.Controllers; 9 | 10 | [Route("api/order")] 11 | [ApiController] 12 | public class OrderQueryController : ControllerBase 13 | { 14 | private readonly IQueryBroker _queryBroker; 15 | 16 | public OrderQueryController( 17 | IQueryBroker queryBroker) 18 | { 19 | _queryBroker = queryBroker; 20 | } 21 | 22 | // GET api/order 23 | [HttpGet] 24 | public async Task GetOrders() 25 | { 26 | var orders = await _queryBroker.SendAsync(new GetOrders()); 27 | var result = MapOrderViews(orders); 28 | return Ok(result); 29 | } 30 | 31 | // GET api/order/customer/id 32 | [HttpGet] 33 | [Route("customer/{id:guid}")] 34 | public async Task GetOrders([FromRoute] Guid id) 35 | { 36 | var orders = await _queryBroker.SendAsync(new GetOrdersByCustomer(id)); 37 | var result = MapOrderViews(orders); 38 | return Ok(result); 39 | } 40 | 41 | // GET api/order/id 42 | [HttpGet] 43 | [Route("{id:guid}")] 44 | public async Task GetOrder([FromRoute] Guid id) 45 | { 46 | var order = await _queryBroker.SendAsync(new GetOrder(id)); 47 | if (order == null) return NotFound(); 48 | var result = MapOrderViews(Enumerable.Repeat(order, 1)).Single(); 49 | return Ok(result); 50 | } 51 | 52 | private IEnumerable MapOrderViews(IEnumerable orders) => 53 | orders.Select(o => new OrderView 54 | { 55 | Id = o!.Id, 56 | CustomerId = o.CustomerId, 57 | OrderDate = o.OrderDate, 58 | OrderTotal = o.OrderItems.Sum(i => i.ProductPrice), 59 | Street = o.ShippingAddress.Street, 60 | City = o.ShippingAddress.City, 61 | State = o.ShippingAddress.State, 62 | Country = o.ShippingAddress.Country, 63 | PostalCode = o.ShippingAddress.PostalCode, 64 | OrderState = (OrderState)o.OrderState, 65 | ETag = o.ETag ?? string.Empty 66 | }); 67 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/DTO/Read/OrderState.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.DTO.Read; 2 | 3 | public enum OrderState 4 | { 5 | Created, 6 | Shipped, 7 | Cancelled 8 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/DTO/Read/OrderView.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.DTO.Read; 2 | 3 | public class OrderView 4 | { 5 | public Guid Id { get; set; } 6 | public Guid CustomerId { get; set; } 7 | public DateTime OrderDate { get; set; } 8 | public decimal OrderTotal { get; set; } 9 | public string Street { get; set; } = null!; 10 | public string City { get; set; } = null!; 11 | public string State { get; set; } = null!; 12 | public string Country { get; set; } = null!; 13 | public string PostalCode { get; set; } = null!; 14 | public OrderState OrderState { get; set; } 15 | public string ETag { get; set; } = null!; 16 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/DTO/Write/Address.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.DTO.Write; 2 | 3 | public class Address 4 | { 5 | public string Street { get; set; } = null!; 6 | public string City { get; set; } = null!; 7 | public string State { get; set; } = null!; 8 | public string Country { get; set; } = null!; 9 | public string PostalCode { get; set; } = null!; 10 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/DTO/Write/Order.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.DTO.Write; 2 | 3 | public class Order 4 | { 5 | public Guid Id { get; set; } 6 | public Guid CustomerId { get; set; } 7 | public DateTime OrderDate { get; set; } 8 | public List OrderItems { get; set; } = null!; 9 | public Address ShippingAddress { get; set; } = null!; 10 | public OrderState OrderState { get; set; } 11 | public string ETag { get; set; } = null!; 12 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/DTO/Write/OrderItem.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.DTO.Write; 2 | 3 | public record OrderItem(Guid ProductId, string ProductName, decimal ProductPrice); -------------------------------------------------------------------------------- /reference-architecture/OrderService/DTO/Write/OrderState.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.DTO.Write; 2 | 3 | public enum OrderState 4 | { 5 | Created, 6 | Shipped, 7 | Cancelled 8 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Address.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.Domain.OrderAggregate; 2 | 3 | public record Address(string Street, string City, string State, string Country, string PostalCode); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/CommandHandlers/CancelOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | using EventDriven.DDD.Abstractions.Repositories; 3 | using OrderService.Domain.OrderAggregate.Commands; 4 | using OrderService.Repositories; 5 | 6 | namespace OrderService.Domain.OrderAggregate.CommandHandlers; 7 | 8 | public class CancelOrderHandler : ICommandHandler 9 | { 10 | private readonly IOrderRepository _repository; 11 | 12 | public CancelOrderHandler( 13 | IOrderRepository repository) 14 | { 15 | _repository = repository; 16 | } 17 | 18 | public async Task> Handle(CancelOrder command, CancellationToken cancellationToken) 19 | { 20 | // Process command 21 | var entity = await _repository.GetAsync(command.EntityId); 22 | if (entity == null) return new CommandResult(CommandOutcome.NotFound); 23 | var domainEvent = entity.Process(command); 24 | 25 | // Apply events 26 | entity.Apply(domainEvent); 27 | 28 | try 29 | { 30 | // Persist entity 31 | var order = await _repository.UpdateOrderStateAsync(entity, OrderState.Cancelled); 32 | if (order == null) return new CommandResult(CommandOutcome.NotFound); 33 | return new CommandResult(CommandOutcome.Accepted, order); 34 | } 35 | catch (ConcurrencyException) 36 | { 37 | return new CommandResult(CommandOutcome.Conflict); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/CommandHandlers/CreateOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | using OrderService.Domain.OrderAggregate.Commands; 3 | using OrderService.Repositories; 4 | 5 | namespace OrderService.Domain.OrderAggregate.CommandHandlers; 6 | 7 | public class CreateOrderHandler : ICommandHandler 8 | { 9 | private readonly IOrderRepository _repository; 10 | 11 | public CreateOrderHandler( 12 | IOrderRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task> Handle(CreateOrder command, CancellationToken cancellationToken) 18 | { 19 | // Process command 20 | if (command.Entity == null) return new CommandResult(CommandOutcome.InvalidCommand); 21 | var domainEvent = command.Entity.Process(command); 22 | 23 | // Apply events 24 | command.Entity.Apply(domainEvent); 25 | 26 | // Persist entity 27 | var entity = await _repository.AddAsync(command.Entity); 28 | if (entity == null) return new CommandResult(CommandOutcome.InvalidCommand); 29 | return new CommandResult(CommandOutcome.Accepted, entity); 30 | } 31 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/CommandHandlers/RemoveOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | using OrderService.Domain.OrderAggregate.Commands; 3 | using OrderService.Repositories; 4 | 5 | namespace OrderService.Domain.OrderAggregate.CommandHandlers; 6 | 7 | public class RemoveOrderHandler : ICommandHandler 8 | { 9 | private readonly IOrderRepository _repository; 10 | 11 | public RemoveOrderHandler( 12 | IOrderRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task Handle(RemoveOrder command, CancellationToken cancellationToken) 18 | { 19 | // Process command 20 | var entity = await _repository.GetAsync(command.EntityId); 21 | if (entity == null) return new CommandResult(CommandOutcome.NotFound); 22 | var domainEvent = entity.Process(command); 23 | 24 | // Apply events 25 | entity.Apply(domainEvent); 26 | 27 | // Persist entity 28 | await _repository.RemoveAsync(command.EntityId); 29 | return new CommandResult(CommandOutcome.Accepted); 30 | } 31 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/CommandHandlers/ShipOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | using EventDriven.DDD.Abstractions.Repositories; 3 | using OrderService.Domain.OrderAggregate.Commands; 4 | using OrderService.Repositories; 5 | 6 | namespace OrderService.Domain.OrderAggregate.CommandHandlers; 7 | 8 | public class ShipOrderHandler : ICommandHandler 9 | { 10 | private readonly IOrderRepository _repository; 11 | 12 | public ShipOrderHandler( 13 | IOrderRepository repository) 14 | { 15 | _repository = repository; 16 | } 17 | 18 | public async Task> Handle(ShipOrder command, CancellationToken cancellationToken) 19 | { 20 | // Process command 21 | var entity = await _repository.GetAsync(command.EntityId); 22 | if (entity == null) return new CommandResult(CommandOutcome.NotFound); 23 | var domainEvent = entity.Process(command); 24 | 25 | // Apply events 26 | entity.Apply(domainEvent); 27 | 28 | try 29 | { 30 | // Persist entity 31 | var order = await _repository.UpdateOrderStateAsync(entity, OrderState.Shipped); 32 | if (order == null) return new CommandResult(CommandOutcome.NotFound); 33 | return new CommandResult(CommandOutcome.Accepted, order); 34 | } 35 | catch (ConcurrencyException) 36 | { 37 | return new CommandResult(CommandOutcome.Conflict); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/CommandHandlers/UpdateOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | using EventDriven.DDD.Abstractions.Repositories; 3 | using OrderService.Domain.OrderAggregate.Commands; 4 | using OrderService.Repositories; 5 | 6 | namespace OrderService.Domain.OrderAggregate.CommandHandlers; 7 | 8 | public class UpdateOrderHandler : ICommandHandler 9 | { 10 | private readonly IOrderRepository _repository; 11 | 12 | public UpdateOrderHandler( 13 | IOrderRepository repository) 14 | { 15 | _repository = repository; 16 | } 17 | 18 | public async Task> Handle(UpdateOrder command, CancellationToken cancellationToken) 19 | { 20 | if (command.Entity == null) return new CommandResult(CommandOutcome.InvalidCommand); 21 | 22 | try 23 | { 24 | // Persist entity 25 | var entity = await _repository.UpdateAsync(command.Entity); 26 | if (entity == null) return new CommandResult(CommandOutcome.NotFound); 27 | return new CommandResult(CommandOutcome.Accepted, entity); 28 | } 29 | catch (ConcurrencyException) 30 | { 31 | return new CommandResult(CommandOutcome.Conflict); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Commands/CancelOrder.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Commands; 4 | 5 | public record CancelOrder(Guid EntityId) : Command(null, EntityId); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Commands/CreateOrder.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Commands; 4 | 5 | public record CreateOrder(Order Entity) : Command(Entity); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Commands/RemoveOrder.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Commands; 4 | 5 | public record RemoveOrder(Guid EntityId) : Command(EntityId); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Commands/ShipOrder.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Commands; 4 | 5 | public record ShipOrder(Guid EntityId) : Command(null, EntityId); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Commands/UpdateOrder.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Commands; 4 | 5 | public record UpdateOrder(Order Entity) : Command(Entity); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Events/OrderCancelled.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Events; 4 | 5 | public record OrderCancelled(Guid EntityId) : DomainEvent(null, EntityId); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Events/OrderCreated.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Events; 4 | 5 | public record OrderCreated(Order? Entity) : DomainEvent(Entity); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Events/OrderRemoved.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Events; 4 | 5 | public record OrderRemoved(Guid EntityId) : DomainEvent(EntityId); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Events/OrderShipped.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Events; 4 | 5 | public record OrderShipped(Guid EntityId) : DomainEvent(EntityId); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Events/OrderUpdated.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.DDD.Abstractions.Events; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Events; 4 | 5 | public record OrderUpdated(Order? Entity) : DomainEvent(Entity); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Order.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Commands; 2 | using EventDriven.DDD.Abstractions.Entities; 3 | using EventDriven.DDD.Abstractions.Events; 4 | using OrderService.Domain.OrderAggregate.Commands; 5 | using OrderService.Domain.OrderAggregate.Events; 6 | 7 | namespace OrderService.Domain.OrderAggregate; 8 | 9 | public class Order : 10 | Entity, 11 | ICommandProcessor, 12 | IEventApplier, 13 | ICommandProcessor, 14 | IEventApplier, 15 | ICommandProcessor, 16 | IEventApplier, 17 | ICommandProcessor, 18 | IEventApplier, 19 | ICommandProcessor, 20 | IEventApplier 21 | { 22 | public Guid CustomerId { get; set; } 23 | public DateTime OrderDate { get; set; } 24 | public List OrderItems { get; set; } = null!; 25 | public Address ShippingAddress { get; set; } = null!; 26 | public OrderState OrderState { get; set; } 27 | 28 | public OrderCreated Process(CreateOrder command) 29 | // To process command, return one or more domain events 30 | => new(command.Entity); 31 | 32 | public void Apply(OrderCreated domainEvent) => 33 | // Set Id 34 | Id = domainEvent.EntityId != default ? domainEvent.EntityId : Guid.NewGuid(); 35 | 36 | public OrderUpdated Process(UpdateOrder command) => 37 | // To process command, return a domain event 38 | new(command.Entity); 39 | 40 | public void Apply(OrderUpdated domainEvent) 41 | { 42 | // Set ETag 43 | if (domainEvent.EntityETag != null) ETag = domainEvent.EntityETag; 44 | } 45 | 46 | public OrderRemoved Process(RemoveOrder command) => 47 | // To process command, return a domain event 48 | new(command.EntityId); 49 | 50 | public void Apply(OrderRemoved domainEvent) 51 | { 52 | // Could mutate state here to implement a soft delete 53 | } 54 | public OrderShipped Process(ShipOrder command) => 55 | // To process command, return one or more domain events 56 | new(command.EntityId); 57 | 58 | public void Apply(OrderShipped domainEvent) 59 | { 60 | // To apply events, mutate the entity state 61 | OrderState = OrderState.Shipped; 62 | if (domainEvent.EntityETag != null) ETag = domainEvent.EntityETag; 63 | } 64 | 65 | public OrderCancelled Process(CancelOrder command) => 66 | // To process command, return one or more domain events 67 | new(command.EntityId); 68 | 69 | public void Apply(OrderCancelled domainEvent) 70 | { 71 | // To apply events, mutate the entity state 72 | OrderState = OrderState.Cancelled; 73 | if (domainEvent.EntityETag != null) ETag = domainEvent.EntityETag; 74 | } 75 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/OrderItem.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.Domain.OrderAggregate; 2 | 3 | public record OrderItem(Guid ProductId, string ProductName, decimal ProductPrice); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/OrderState.cs: -------------------------------------------------------------------------------- 1 | namespace OrderService.Domain.OrderAggregate; 2 | 3 | public enum OrderState 4 | { 5 | Created, 6 | Shipped, 7 | Cancelled 8 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Queries/GetOrder.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Queries; 4 | 5 | public record GetOrder(Guid Id) : Query; -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Queries/GetOrders.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Queries; 4 | 5 | public record GetOrders : Query>; -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/Queries/GetOrdersByCustomer.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | 3 | namespace OrderService.Domain.OrderAggregate.Queries; 4 | 5 | public record GetOrdersByCustomer(Guid CustomerId) : Query>; -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/QueryHandlers/GetOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | using OrderService.Domain.OrderAggregate.Queries; 3 | using OrderService.Repositories; 4 | 5 | namespace OrderService.Domain.OrderAggregate.QueryHandlers; 6 | 7 | public class GetOrderHandler : IQueryHandler 8 | { 9 | private readonly IOrderRepository _repository; 10 | 11 | public GetOrderHandler( 12 | IOrderRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task Handle(GetOrder query, CancellationToken cancellationToken) 18 | { 19 | var result = await _repository.GetAsync(query.Id); 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/QueryHandlers/GetOrdersByCustomerHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | using OrderService.Domain.OrderAggregate.Queries; 3 | using OrderService.Repositories; 4 | 5 | namespace OrderService.Domain.OrderAggregate.QueryHandlers; 6 | 7 | public class GetOrdersByCustomerHandler : IQueryHandler> 8 | { 9 | private readonly IOrderRepository _repository; 10 | 11 | public GetOrdersByCustomerHandler( 12 | IOrderRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task> Handle(GetOrdersByCustomer query, CancellationToken cancellationToken) 18 | { 19 | // Retrieve entities 20 | var result = await _repository.GetByCustomerAsync(query.CustomerId); 21 | return result; 22 | } 23 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Domain/OrderAggregate/QueryHandlers/GetOrdersHandler.cs: -------------------------------------------------------------------------------- 1 | using EventDriven.CQRS.Abstractions.Queries; 2 | using OrderService.Domain.OrderAggregate.Queries; 3 | using OrderService.Repositories; 4 | 5 | namespace OrderService.Domain.OrderAggregate.QueryHandlers; 6 | 7 | public class GetOrdersHandler : IQueryHandler> 8 | { 9 | private readonly IOrderRepository _repository; 10 | 11 | public GetOrdersHandler( 12 | IOrderRepository repository) 13 | { 14 | _repository = repository; 15 | } 16 | 17 | public async Task> Handle(GetOrders query, CancellationToken cancellationToken) 18 | { 19 | // Retrieve entities 20 | var result = await _repository.GetAsync(); 21 | return result; 22 | } 23 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Integration/EventHandlers/CustomerAddressUpdatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EventDriven.EventBus.Abstractions; 3 | using Common.Integration.Events; 4 | using OrderService.Domain.OrderAggregate; 5 | using OrderService.Repositories; 6 | 7 | namespace OrderService.Integration.EventHandlers; 8 | 9 | public class CustomerAddressUpdatedEventHandler : IntegrationEventHandler 10 | { 11 | private readonly IOrderRepository _orderRepository; 12 | private readonly IMapper _mapper; 13 | private readonly ILogger _logger; 14 | 15 | public CustomerAddressUpdatedEventHandler(IOrderRepository orderRepository, 16 | IMapper mapper, 17 | ILogger logger) 18 | { 19 | _orderRepository = orderRepository; 20 | _mapper = mapper; 21 | _logger = logger; 22 | } 23 | 24 | public override async Task HandleAsync(CustomerAddressUpdated @event) 25 | { 26 | _logger.LogInformation("----- Handling CustomerAddressUpdated event"); 27 | var orders = await _orderRepository.GetByCustomerAsync(@event.CustomerId); 28 | foreach (var order in orders) 29 | { 30 | var shippingAddress = _mapper.Map
(@event.ShippingAddress); 31 | await _orderRepository.UpdateAddressAsync(order.Id, shippingAddress); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Mapping/AutoMapperProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using OrderService.Domain.OrderAggregate; 3 | using Address = OrderService.Domain.OrderAggregate.Address; 4 | using IntegrationAddress = Common.Integration.Models.Address; 5 | 6 | namespace OrderService.Mapping; 7 | 8 | public class AutoMapperProfile : Profile 9 | { 10 | public AutoMapperProfile() 11 | { 12 | CreateMap(); 13 | CreateMap().ReverseMap(); 14 | CreateMap(); 15 | CreateMap().ReverseMap(); 16 | 17 | CreateMap() 18 | .IncludeMembers(c => c.ShippingAddress); 19 | CreateMap() 20 | .IncludeMembers(c => c.ShippingAddress).ReverseMap(); 21 | CreateMap(); 22 | CreateMap().ReverseMap(); 23 | 24 | CreateMap(); 25 | CreateMap().ReverseMap(); 26 | CreateMap(); 27 | CreateMap().ReverseMap(); 28 | 29 | CreateMap(); 30 | CreateMap().ReverseMap(); 31 | } 32 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/OrderService.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /reference-architecture/OrderService/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Behaviors; 2 | using EventDriven.CQRS.Abstractions.DependencyInjection; 3 | using EventDriven.DependencyInjection.URF.Mongo; 4 | using MediatR; 5 | using OrderService.Configuration; 6 | using OrderService.Domain.OrderAggregate; 7 | using OrderService.Integration.EventHandlers; 8 | using OrderService.Repositories; 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | // Add Aspire service defaults 13 | builder.AddServiceDefaults(); 14 | 15 | // Add services to the container. 16 | builder.Services.AddControllers(); 17 | builder.Services.AddEndpointsApiExplorer(); 18 | builder.Services.AddSwaggerGen(); 19 | builder.Services.AddProblemDetails(); 20 | 21 | // Add automapper 22 | builder.Services.AddAutoMapper(typeof(Program)); 23 | 24 | // Add command and query handlers 25 | builder.Services.AddHandlers(typeof(Program)); 26 | 27 | // Add behaviors 28 | builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); 29 | 30 | // Add database settings 31 | builder.Services.AddSingleton(); 32 | builder.Services.AddMongoDbSettings(builder.Configuration); 33 | 34 | // Add event handlers 35 | builder.Services.AddSingleton(); 36 | 37 | // Add Dapr event bus 38 | builder.Services.AddDaprEventBus(builder.Configuration); 39 | builder.Services.AddMongoEventCache(builder.Configuration); 40 | 41 | var app = builder.Build(); 42 | 43 | // Configure the HTTP request pipeline. 44 | if (app.Environment.IsDevelopment()) 45 | { 46 | app.UseSwagger(); 47 | app.UseSwaggerUI(); 48 | } 49 | 50 | app.UseRouting(); 51 | app.UseAuthorization(); 52 | 53 | // Use Cloud Events (needed by Dapr) 54 | app.UseCloudEvents(); 55 | 56 | app.MapControllers(); 57 | 58 | // Map Dapr subscriber (needed by Dapr) 59 | app.MapSubscribeHandler(); 60 | 61 | // Map Dapr Event Bus subscribers 62 | app.MapDaprEventBus(eventBus => // used by event bus 63 | { 64 | var customerAddressUpdatedEventHandler = app.Services.GetRequiredService(); 65 | eventBus?.Subscribe(customerAddressUpdatedEventHandler, null, "v1"); 66 | }); 67 | 68 | app.Run(); -------------------------------------------------------------------------------- /reference-architecture/OrderService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:5757;http://localhost:5758", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "specs": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "swagger", 19 | "applicationUrl": "https://localhost:5757;http://localhost:5758", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Specs" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /reference-architecture/OrderService/Repositories/IOrderRepository.cs: -------------------------------------------------------------------------------- 1 | using OrderService.Domain.OrderAggregate; 2 | 3 | namespace OrderService.Repositories; 4 | 5 | public interface IOrderRepository 6 | { 7 | Task> GetAsync(); 8 | Task> GetByCustomerAsync(Guid customerId); 9 | Task GetAsync(Guid id); 10 | Task AddAsync(Order entity); 11 | Task UpdateAsync(Order entity); 12 | Task UpdateAddressAsync(Guid orderId, Address address); 13 | Task RemoveAsync(Guid id); 14 | Task UpdateOrderStateAsync(Order entity, OrderState orderState); 15 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/Repositories/OrderRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using EventDriven.DDD.Abstractions.Repositories; 3 | using MongoDB.Driver; 4 | using OrderService.Domain.OrderAggregate; 5 | using URF.Core.Mongo; 6 | 7 | namespace OrderService.Repositories; 8 | 9 | [ExcludeFromCodeCoverage] 10 | public class OrderRepository : DocumentRepository, IOrderRepository 11 | { 12 | public OrderRepository(IMongoCollection collection) : base(collection) 13 | { 14 | } 15 | public async Task> GetAsync() => 16 | await FindManyAsync(); 17 | 18 | public async Task> GetByCustomerAsync(Guid customerId) => 19 | await FindManyAsync(e => e.CustomerId == customerId); 20 | 21 | public async Task GetAsync(Guid id) => 22 | await FindOneAsync(e => e.Id == id); 23 | 24 | public async Task AddAsync(Order entity) 25 | { 26 | var existing = await FindOneAsync(e => e.Id == entity.Id); 27 | if (existing != null) return null; 28 | if (string.IsNullOrWhiteSpace(entity.ETag)) 29 | entity.ETag = Guid.NewGuid().ToString(); 30 | return await InsertOneAsync(entity); 31 | } 32 | 33 | public async Task UpdateAsync(Order entity) 34 | { 35 | var existing = await GetAsync(entity.Id); 36 | if (existing == null) return null; 37 | if (string.Compare(entity.ETag, existing.ETag, StringComparison.OrdinalIgnoreCase) != 0 ) 38 | throw new ConcurrencyException(); 39 | entity.ETag = Guid.NewGuid().ToString(); 40 | return await FindOneAndReplaceAsync(e => e.Id == entity.Id, entity); 41 | } 42 | 43 | public async Task UpdateAddressAsync(Guid orderId, Address address) 44 | { 45 | var existing = await GetAsync(orderId); 46 | if (existing == null) return null; 47 | existing.ShippingAddress = address; 48 | return await FindOneAndReplaceAsync(e => e.Id == orderId, existing); 49 | } 50 | 51 | public async Task RemoveAsync(Guid id) => 52 | await DeleteOneAsync(e => e.Id == id); 53 | 54 | public async Task UpdateOrderStateAsync(Order entity, OrderState orderState) 55 | { 56 | var existing = await GetAsync(entity.Id); 57 | if (existing == null) return null; 58 | if (string.Compare(entity.ETag, existing.ETag, StringComparison.OrdinalIgnoreCase) != 0 ) 59 | throw new ConcurrencyException(); 60 | entity.ETag = Guid.NewGuid().ToString(); 61 | entity.OrderState = orderState; 62 | return await FindOneAndReplaceAsync(e => e.Id == entity.Id, entity); 63 | } 64 | } -------------------------------------------------------------------------------- /reference-architecture/OrderService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /reference-architecture/OrderService/appsettings.Specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "OrderDatabaseSettings": { 10 | "ConnectionString": "mongodb://localhost:27017", 11 | "DatabaseName": "OrdersTestDb", 12 | "CollectionName": "Orders" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /reference-architecture/OrderService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "OrderDatabaseSettings": { 11 | "ConnectionString": "mongodb://localhost:27017", 12 | "DatabaseName": "OrdersDb", 13 | "CollectionName": "Orders" 14 | }, 15 | "DaprEventBusOptions": { 16 | "PubSubName": "pubsub" 17 | }, 18 | "MongoEventCacheOptions": { 19 | "AppName": "order-service" 20 | }, 21 | "MongoStoreDatabaseSettings": { 22 | "ConnectionString": "mongodb://localhost:27017", 23 | "DatabaseName": "daprStore", 24 | "CollectionName": "daprCollection" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.AppHost/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = DistributedApplication.CreateBuilder(args); 2 | 3 | var pubSub = builder.AddDaprPubSub("pubsub"); 4 | 5 | builder.AddProject("customer-service") 6 | .WithDaprSidecar() 7 | .WithReference(pubSub); 8 | builder.AddProject("order-service") 9 | .WithDaprSidecar() 10 | .WithReference(pubSub); 11 | 12 | builder.Build().Run(); 13 | -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.AppHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:17098;http://localhost:15138", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16117", 13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:16118" 14 | } 15 | }, 16 | "specs": { 17 | "commandName": "Project", 18 | "dotnetRunMessages": true, 19 | "launchBrowser": true, 20 | "applicationUrl": "https://localhost:17098;http://localhost:15138", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Specs", 23 | "DOTNET_ENVIRONMENT": "Specs", 24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16117", 25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:16118" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.AppHost/ReferenceArchitecture.AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning", 7 | "Aspire.Hosting.Dcp": "Warning" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.ServiceDefaults/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Diagnostics.HealthChecks; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.ServiceDiscovery; 7 | using OpenTelemetry; 8 | using OpenTelemetry.Metrics; 9 | using OpenTelemetry.Trace; 10 | 11 | namespace Microsoft.Extensions.Hosting; 12 | 13 | // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. 14 | // This project should be referenced by each service project in your solution. 15 | // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults 16 | public static class Extensions 17 | { 18 | public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) 19 | { 20 | builder.ConfigureOpenTelemetry(); 21 | 22 | builder.AddDefaultHealthChecks(); 23 | 24 | builder.Services.AddServiceDiscovery(); 25 | 26 | builder.Services.ConfigureHttpClientDefaults(http => 27 | { 28 | // Turn on resilience by default 29 | http.AddStandardResilienceHandler(); 30 | 31 | // Turn on service discovery by default 32 | http.AddServiceDiscovery(); 33 | }); 34 | 35 | // Uncomment the following to restrict the allowed schemes for service discovery. 36 | // builder.Services.Configure(options => 37 | // { 38 | // options.AllowedSchemes = ["https"]; 39 | // }); 40 | 41 | return builder; 42 | } 43 | 44 | public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) 45 | { 46 | builder.Logging.AddOpenTelemetry(logging => 47 | { 48 | logging.IncludeFormattedMessage = true; 49 | logging.IncludeScopes = true; 50 | }); 51 | 52 | builder.Services.AddOpenTelemetry() 53 | .WithMetrics(metrics => 54 | { 55 | metrics.AddAspNetCoreInstrumentation() 56 | .AddHttpClientInstrumentation() 57 | .AddRuntimeInstrumentation(); 58 | }) 59 | .WithTracing(tracing => 60 | { 61 | tracing.AddAspNetCoreInstrumentation() 62 | // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) 63 | //.AddGrpcClientInstrumentation() 64 | .AddHttpClientInstrumentation(); 65 | }); 66 | 67 | builder.AddOpenTelemetryExporters(); 68 | 69 | return builder; 70 | } 71 | 72 | private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) 73 | { 74 | var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); 75 | 76 | if (useOtlpExporter) 77 | { 78 | builder.Services.AddOpenTelemetry().UseOtlpExporter(); 79 | } 80 | 81 | // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) 82 | //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) 83 | //{ 84 | // builder.Services.AddOpenTelemetry() 85 | // .UseAzureMonitor(); 86 | //} 87 | 88 | return builder; 89 | } 90 | 91 | public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) 92 | { 93 | builder.Services.AddHealthChecks() 94 | // Add a default liveness check to ensure app is responsive 95 | .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); 96 | 97 | return builder; 98 | } 99 | 100 | public static WebApplication MapDefaultEndpoints(this WebApplication app) 101 | { 102 | // Adding health checks endpoints to applications in non-development environments has security implications. 103 | // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. 104 | if (app.Environment.IsDevelopment()) 105 | { 106 | // All health checks must pass for app to be considered ready to accept traffic after starting 107 | app.MapHealthChecks("/health"); 108 | 109 | // Only health checks tagged with the "live" tag must pass for app to be considered alive 110 | app.MapHealthChecks("/alive", new HealthCheckOptions 111 | { 112 | Predicate = r => r.Tags.Contains("live") 113 | }); 114 | } 115 | 116 | return app; 117 | } 118 | } -------------------------------------------------------------------------------- /reference-architecture/ReferenceArchitecture.ServiceDefaults/ReferenceArchitecture.ServiceDefaults.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net8.0 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /reference-architecture/dapr/components/pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: pubsub 5 | spec: 6 | type: pubsub.redis 7 | metadata: 8 | - name: redisHost 9 | value: localhost:6379 10 | - name: redisPassword 11 | value: "" 12 | -------------------------------------------------------------------------------- /reference-architecture/dapr/components/statestore-mongodb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: statestore-mongodb 5 | spec: 6 | type: state.mongodb 7 | metadata: 8 | - name: host 9 | value: localhost:27017 10 | -------------------------------------------------------------------------------- /reference-architecture/dapr/components/statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: statestore 5 | spec: 6 | type: state.redis 7 | metadata: 8 | - name: redisHost 9 | value: localhost:6379 10 | - name: redisPassword 11 | value: "" 12 | - name: actorStateStore 13 | value: "true" 14 | -------------------------------------------------------------------------------- /reference-architecture/dapr/standby/pubsub-snssqs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: pubsub-snssqs 5 | spec: 6 | type: pubsub.snssqs 7 | version: v1 8 | metadata: 9 | - name: endpoint 10 | value: http://localhost:4566 11 | # Use us-east-1 for localstack 12 | - name: region 13 | value: us-east-1 14 | -------------------------------------------------------------------------------- /reference-architecture/json/customers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "eTag": "", 5 | "firstName": "Elon", 6 | "lastName": "Musk", 7 | "shippingAddress": { 8 | "street": "123 This Street", 9 | "city": "Freemont", 10 | "state": "CA", 11 | "country": "USA", 12 | "postalCode": "90045" 13 | } 14 | }, 15 | { 16 | "id": "848f5790-3981-4862-bb7e-a8566aa07026", 17 | "eTag": "", 18 | "firstName": "Jeff", 19 | "lastName": "Bezos", 20 | "shippingAddress": { 21 | "street": "123 That Street", 22 | "city": "Seattle", 23 | "state": "WA", 24 | "country": "USA", 25 | "postalCode": "54321" 26 | } 27 | }, 28 | { 29 | "id": "1c44eea7-400a-4f6f-ab99-5e8c853ea363", 30 | "eTag": "", 31 | "firstName": "Mark", 32 | "lastName": "Zuckerberg", 33 | "shippingAddress": { 34 | "street": "123 Other Street", 35 | "city": "Palo Alto", 36 | "state": "CA", 37 | "country": "USA", 38 | "postalCode": "98765" 39 | } 40 | } 41 | ] -------------------------------------------------------------------------------- /reference-architecture/json/orders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 4 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 5 | "orderDate": "2021-04-15T22:54:44.485Z", 6 | "orderItems": [ 7 | { 8 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 9 | "productName": "Espresso", 10 | "productPrice": 2.50 11 | }, 12 | { 13 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 14 | "productName": "Cappuccino", 15 | "productPrice": 3.50 16 | } 17 | ], 18 | "shippingAddress": { 19 | "street": "123 This Street", 20 | "city": "Freemont", 21 | "state": "CA", 22 | "country": "USA", 23 | "postalCode": "90045" 24 | }, 25 | "orderState": 0, 26 | "eTag": "" 27 | }, 28 | { 29 | "id": "fd06384d-24cd-4a7f-a5f5-b05ec683bfdd", 30 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 31 | "orderDate": "2021-04-15T22:54:44.485Z", 32 | "orderItems": [ 33 | { 34 | "productId": "42b27777-6eff-4193-828d-5fe554a705d1", 35 | "productName": "Chai", 36 | "productPrice": 1.50 37 | } 38 | ], 39 | "shippingAddress": { 40 | "street": "123 This Street", 41 | "city": "Freemont", 42 | "state": "CA", 43 | "country": "USA", 44 | "postalCode": "90045" 45 | }, 46 | "orderState": 0, 47 | "eTag": "" 48 | } 49 | ] -------------------------------------------------------------------------------- /test/CustomerService.Tests/Controllers/CustomerCommandControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using CustomerService.Controllers; 5 | using CustomerService.Domain.CustomerAggregate; 6 | using CustomerService.Domain.CustomerAggregate.Commands; 7 | using CustomerService.Tests.Fakes; 8 | using CustomerService.Tests.Helpers; 9 | using EventDriven.CQRS.Abstractions.Commands; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Moq; 12 | using Xunit; 13 | 14 | namespace CustomerService.Tests.Controllers; 15 | 16 | public class CustomerCommandControllerTests 17 | { 18 | private readonly Mock _commandBrokerMoq; 19 | private readonly IMapper _mapper; 20 | 21 | public CustomerCommandControllerTests() 22 | { 23 | _commandBrokerMoq = new Mock(); 24 | _mapper = MappingHelper.GetMapper(); 25 | } 26 | 27 | [Fact] 28 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 29 | { 30 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 31 | 32 | Assert.IsAssignableFrom(controller); 33 | Assert.IsType(controller); 34 | } 35 | 36 | [Fact] 37 | public async Task GivenWeAreCreatingACustomer_WhenSuccessful_ThenShouldProvideNewEntityWithPath() 38 | { 39 | var customerOut = _mapper.Map(Customers.Customer1); 40 | 41 | _commandBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 42 | .ReturnsAsync(new CommandResult(CommandOutcome.Accepted, customerOut)); 43 | 44 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 45 | 46 | var actionResult = await controller.Create(Customers.Customer1); 47 | var createdResult = actionResult as CreatedResult; 48 | 49 | Assert.NotNull(actionResult); 50 | Assert.NotNull(createdResult); 51 | Assert.Equal($"api/customer/{customerOut.Id}", createdResult.Location, true); 52 | } 53 | 54 | [Fact] 55 | public async Task GivenWeAreCreatingACustomer_WhenFailure_ThenShouldReturnError() 56 | { 57 | _commandBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 58 | .ReturnsAsync(new CommandResult(CommandOutcome.NotHandled)); 59 | 60 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 61 | 62 | var actionResult = await controller.Create(Customers.Customer1); 63 | var statusCodeResult = actionResult as StatusCodeResult; 64 | 65 | Assert.NotNull(actionResult); 66 | Assert.NotNull(statusCodeResult); 67 | Assert.Equal(500, statusCodeResult.StatusCode); 68 | } 69 | 70 | [Fact] 71 | public async Task GivenWeAreUpdatingACustomer_WhenSuccessful_ThenUpdatedEntityShouldBeReturned() 72 | { 73 | var customerIn = Customers.Customer2; 74 | var customerOut = _mapper.Map(Customers.Customer2); 75 | 76 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 77 | 78 | _commandBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 79 | .ReturnsAsync(new CommandResult(CommandOutcome.Accepted, customerOut)); 80 | 81 | var actionResult = await controller.Update(customerIn); 82 | var objectResult = actionResult as OkObjectResult; 83 | 84 | Assert.NotNull(actionResult); 85 | Assert.NotNull(objectResult); 86 | Assert.Equal(customerIn.Id, ((DTO.Write.Customer)objectResult.Value!).Id); 87 | } 88 | 89 | [Fact] 90 | public async Task GivenWeAreUpdatingACustomer_WhenCustomerDoesNotExist_ThenShouldReturnNotFound() 91 | { 92 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 93 | 94 | _commandBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 95 | .ReturnsAsync(new CommandResult(CommandOutcome.NotFound)); 96 | 97 | var actionResult = await controller.Update(Customers.Customer2); 98 | var notFoundResult = actionResult as NotFoundResult; 99 | 100 | Assert.NotNull(actionResult); 101 | Assert.NotNull(notFoundResult); 102 | } 103 | 104 | [Fact] 105 | public async Task GivenWeAreUpdatingACustomer_WhenWeEncounterAConcurrencyIssue_ThenShouldReturnConflict() 106 | { 107 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 108 | 109 | _commandBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 110 | .ReturnsAsync(new CommandResult(CommandOutcome.Conflict)); 111 | 112 | var actionResult = await controller.Update(Customers.Customer2); 113 | var conflictResult = actionResult as ConflictResult; 114 | 115 | Assert.NotNull(actionResult); 116 | Assert.NotNull(conflictResult); 117 | } 118 | 119 | [Fact] 120 | public async Task GivenWeAreRemovingACustomer_WhenSuccessful_ThenShouldReturnSuccess() 121 | { 122 | var customerId = Guid.NewGuid(); 123 | var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper); 124 | 125 | _commandBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 126 | .ReturnsAsync(new CommandResult(CommandOutcome.Accepted)); 127 | 128 | var actionResult = await controller.Remove(customerId); 129 | var noContentResult = actionResult as NoContentResult; 130 | 131 | Assert.NotNull(actionResult); 132 | Assert.NotNull(noContentResult); 133 | } 134 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Controllers/CustomerQueryControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using CustomerService.Controllers; 5 | using CustomerService.Domain.CustomerAggregate; 6 | using CustomerService.Domain.CustomerAggregate.Queries; 7 | using CustomerService.DTO.Read; 8 | using CustomerService.Tests.Fakes; 9 | using CustomerService.Tests.Helpers; 10 | using EventDriven.CQRS.Abstractions.Queries; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Moq; 13 | using Xunit; 14 | 15 | namespace CustomerService.Tests.Controllers; 16 | 17 | public class CustomerQueryControllerTests 18 | { 19 | private readonly Mock _queryBrokerMoq; 20 | private readonly IMapper _mapper; 21 | 22 | public CustomerQueryControllerTests() 23 | { 24 | _queryBrokerMoq = new Mock(); 25 | _mapper = MappingHelper.GetMapper(); 26 | } 27 | 28 | [Fact] 29 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 30 | { 31 | var controller = new CustomerQueryController(_queryBrokerMoq.Object, _mapper); 32 | 33 | Assert.IsAssignableFrom(controller); 34 | Assert.IsType(controller); 35 | } 36 | 37 | [Fact] 38 | public async Task WhenRetrievingAllCustomers_ThenAllCustomersShouldBeReturned() 39 | { 40 | _queryBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 41 | .ReturnsAsync(new List 42 | { 43 | _mapper.Map(Customers.Customer1), 44 | _mapper.Map(Customers.Customer2), 45 | _mapper.Map(Customers.Customer3) 46 | }); 47 | 48 | var controller = new CustomerQueryController(_queryBrokerMoq.Object, _mapper); 49 | 50 | var actionResult = await controller.GetCustomers(); 51 | var okResult = Assert.IsType(actionResult); 52 | var value = (IEnumerable)okResult.Value!; 53 | 54 | Assert.Collection(value, 55 | c => Assert.Equal(CustomerViews.Customer1.Id, c.Id), 56 | c => Assert.Equal(CustomerViews.Customer2.Id, c.Id), 57 | c => Assert.Equal(CustomerViews.Customer3.Id, c.Id)); 58 | } 59 | 60 | [Fact] 61 | public async Task GivenWeAreRetrievingACustomerById_WhenSuccessful_ThenCorrectCustomerShouldBeReturned() 62 | { 63 | _queryBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 64 | .ReturnsAsync(_mapper.Map(Customers.Customer1)); 65 | 66 | var controller = new CustomerQueryController(_queryBrokerMoq.Object, _mapper); 67 | 68 | var actionResult = await controller.GetCustomer(Customers.Customer1.Id); 69 | var okResult = Assert.IsType(actionResult); 70 | var value = (CustomerView)okResult.Value!; 71 | 72 | Assert.Equal(CustomerViews.Customer1.Id, value.Id); 73 | } 74 | 75 | [Fact] 76 | public async Task GivenWeAreRetrievingACustomerById_WhenCustomerIsNotFound_ThenShouldReturnNotFound() 77 | { 78 | _queryBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 79 | .ReturnsAsync((Customer)null!); 80 | 81 | var controller = new CustomerQueryController(_queryBrokerMoq.Object, _mapper); 82 | 83 | var actionResult = await controller.GetCustomer(Customers.Customer1.Id); 84 | var notFoundResult = actionResult as NotFoundResult; 85 | 86 | Assert.NotNull(actionResult); 87 | Assert.NotNull(notFoundResult); 88 | } 89 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/CustomerService.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/CustomerService.Tests/Domain/CustomerAggregate/CommandHandlers/CreateCustomerHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CustomerService.Domain.CustomerAggregate; 3 | using CustomerService.Domain.CustomerAggregate.Commands; 4 | using CustomerService.Domain.CustomerAggregate.CommandHandlers; 5 | using CustomerService.Repositories; 6 | using EventDriven.CQRS.Abstractions.Commands; 7 | using Moq; 8 | using Xunit; 9 | 10 | namespace CustomerService.Tests.Domain.CustomerAggregate.CommandHandlers; 11 | 12 | public class CreateCustomerHandlerTests 13 | { 14 | private readonly Mock _repositoryMock; 15 | 16 | public CreateCustomerHandlerTests() 17 | { 18 | _repositoryMock = new Mock(); 19 | } 20 | 21 | [Fact] 22 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 23 | { 24 | var handler = new CreateCustomerHandler(_repositoryMock.Object); 25 | 26 | Assert.NotNull(handler); 27 | Assert.IsType(handler); 28 | } 29 | 30 | [Fact] 31 | public async Task WhenCreatingEntityFails_ThenShouldReturnFailure() 32 | { 33 | var handler = new CreateCustomerHandler(_repositoryMock.Object); 34 | 35 | var cmdResult = await handler.Handle(new CreateCustomer(new Customer()), default); 36 | 37 | Assert.Equal(CommandOutcome.InvalidCommand, cmdResult.Outcome); 38 | } 39 | 40 | [Fact] 41 | public async Task WhenEntityIsCreated_ThenShouldReturnSuccess() 42 | { 43 | var customer = new Customer(); 44 | _repositoryMock.Setup(x => x.AddAsync(It.IsAny())) 45 | .ReturnsAsync(customer); 46 | var handler = new CreateCustomerHandler(_repositoryMock.Object); 47 | 48 | var cmdResult = await handler.Handle(new CreateCustomer(customer), default); 49 | 50 | Assert.Equal(CommandOutcome.Accepted, cmdResult.Outcome); 51 | } 52 | 53 | [Fact] 54 | public async Task WhenEntityIsCreated_ThenShouldReturnNewEntity() 55 | { 56 | var customer = new Customer(); 57 | _repositoryMock.Setup(x => x.AddAsync(It.IsAny())) 58 | .ReturnsAsync(customer); 59 | var handler = new CreateCustomerHandler(_repositoryMock.Object); 60 | 61 | var cmdResult = await handler.Handle(new CreateCustomer(customer), default); 62 | 63 | Assert.Equal(customer.Id,cmdResult.Entities[0].Id); 64 | } 65 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Domain/CustomerAggregate/CommandHandlers/RemoveCustomerHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CustomerService.Domain.CustomerAggregate.Commands; 4 | using CustomerService.Domain.CustomerAggregate.CommandHandlers; 5 | using CustomerService.Repositories; 6 | using EventDriven.CQRS.Abstractions.Commands; 7 | using Moq; 8 | using Xunit; 9 | 10 | namespace CustomerService.Tests.Domain.CustomerAggregate.CommandHandlers; 11 | 12 | public class RemoveCustomerHandlerTests 13 | { 14 | 15 | private readonly Mock _repositoryMock; 16 | 17 | public RemoveCustomerHandlerTests() 18 | { 19 | _repositoryMock = new Mock(); 20 | } 21 | 22 | [Fact] 23 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 24 | { 25 | var handler = new RemoveCustomerHandler(_repositoryMock.Object); 26 | 27 | Assert.NotNull(handler); 28 | Assert.IsType(handler); 29 | } 30 | 31 | [Fact] 32 | public async Task WhenEntityIsRemoved_ThenShouldReturnSuccess() 33 | { 34 | var handler = new RemoveCustomerHandler(_repositoryMock.Object); 35 | 36 | var cmdResult = await handler.Handle(new RemoveCustomer(Guid.NewGuid()), default); 37 | 38 | Assert.Equal(CommandOutcome.Accepted, cmdResult.Outcome); 39 | } 40 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Domain/CustomerAggregate/CommandHandlers/UpdateCustomerHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoFixture; 4 | using AutoMapper; 5 | using Common.Integration.Events; 6 | using CustomerService.Domain.CustomerAggregate; 7 | using CustomerService.Domain.CustomerAggregate.Commands; 8 | using CustomerService.Domain.CustomerAggregate.CommandHandlers; 9 | using CustomerService.Repositories; 10 | using CustomerService.Tests.Helpers; 11 | using EventDriven.CQRS.Abstractions.Commands; 12 | using EventDriven.DDD.Abstractions.Repositories; 13 | using EventDriven.EventBus.Abstractions; 14 | using Microsoft.Extensions.Logging; 15 | using Moq; 16 | using Xunit; 17 | 18 | namespace CustomerService.Tests.Domain.CustomerAggregate.CommandHandlers; 19 | 20 | public class UpdateCustomerHandlerTests 21 | { 22 | private readonly Mock _eventBusMock; 23 | private readonly Fixture _fixture = new(); 24 | 25 | private readonly Mock> _loggerMock; 26 | private readonly IMapper _mapper = MappingHelper.GetMapper(); 27 | private readonly Mock _repositoryMock; 28 | 29 | public UpdateCustomerHandlerTests() 30 | { 31 | _loggerMock = new Mock>(); 32 | _repositoryMock = new Mock(); 33 | _eventBusMock = new Mock(); 34 | } 35 | 36 | [Fact] 37 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 38 | { 39 | var handler = GetHandler(); 40 | 41 | Assert.NotNull(handler); 42 | Assert.IsType(handler); 43 | } 44 | 45 | [Fact] 46 | public async Task WhenNoExistingCustomerIsFound_ThenShouldReturnNotFound() 47 | { 48 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 49 | .ReturnsAsync(GenerateCustomer()); 50 | 51 | var handler = GetHandler(); 52 | 53 | var cmdResult = await handler.Handle(new UpdateCustomer(GenerateCustomer()), default); 54 | 55 | Assert.Equal(CommandOutcome.NotFound, cmdResult.Outcome); 56 | } 57 | 58 | [Fact] 59 | public async Task WhenTheAddressIsUpdated_ThenEventShouldBePublished() 60 | { 61 | var existingCustomer = GenerateCustomer(); 62 | var updatedCustomer = GenerateCustomer(); 63 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 64 | .ReturnsAsync(existingCustomer); 65 | _repositoryMock.Setup(x => x.UpdateAsync(It.IsAny())) 66 | .ReturnsAsync(updatedCustomer); 67 | var eventRaised = false; 68 | _eventBusMock.Setup(x => x.PublishAsync( 69 | It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 70 | .Callback(() => eventRaised = true); 71 | var handler = GetHandler(); 72 | 73 | await handler.Handle(new UpdateCustomer(updatedCustomer), default); 74 | 75 | Assert.True(eventRaised); 76 | } 77 | 78 | [Fact] 79 | public async Task WhenTheAddressIsNotUpdated_ThenEventShouldNotBePublished() 80 | { 81 | var existingCustomer = GenerateCustomer(); 82 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 83 | .ReturnsAsync(existingCustomer); 84 | _repositoryMock.Setup(x => x.UpdateAsync(It.IsAny())) 85 | .ReturnsAsync(existingCustomer); 86 | var eventRaised = false; 87 | _eventBusMock.Setup(x => x.PublishAsync( 88 | It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 89 | .Callback(() => eventRaised = true); 90 | var handler = GetHandler(); 91 | 92 | await handler.Handle(new UpdateCustomer(existingCustomer), default); 93 | 94 | Assert.False(eventRaised); 95 | } 96 | 97 | [Fact] 98 | public async Task WhenTheCustomerIsUpdated_ThenShouldReturnSuccess() 99 | { 100 | var existingCustomer = GenerateCustomer(); 101 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 102 | .ReturnsAsync(existingCustomer); 103 | _repositoryMock.Setup(x => x.UpdateAsync(It.IsAny())) 104 | .ReturnsAsync(existingCustomer); 105 | var handler = GetHandler(); 106 | 107 | var cmdResult = await handler.Handle(new UpdateCustomer(existingCustomer), default); 108 | 109 | Assert.Equal(CommandOutcome.Accepted, cmdResult.Outcome); 110 | } 111 | 112 | [Fact] 113 | public async Task WhenConcurrencyExceptionOccurs_ThenShouldReturnConflict() 114 | { 115 | var existingCustomer = GenerateCustomer(); 116 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 117 | .ReturnsAsync(existingCustomer); 118 | _repositoryMock.Setup(x => x.UpdateAsync(It.IsAny())) 119 | .ThrowsAsync(new ConcurrencyException()); 120 | var handler = GetHandler(); 121 | 122 | var cmdResult = await handler.Handle(new UpdateCustomer(existingCustomer), default); 123 | 124 | Assert.Equal(CommandOutcome.Conflict, cmdResult.Outcome); 125 | } 126 | 127 | private UpdateCustomerHandler GetHandler() => 128 | new(_repositoryMock.Object, 129 | _eventBusMock.Object, 130 | _mapper, 131 | _loggerMock.Object); 132 | 133 | private Customer GenerateCustomer() => 134 | _fixture.Build() 135 | .With(x => x.ShippingAddress) 136 | .With(x => x.Id) 137 | .Create(); 138 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Domain/CustomerAggregate/CustomerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CustomerService.Domain.CustomerAggregate; 3 | using CustomerService.Domain.CustomerAggregate.Commands; 4 | using CustomerService.Domain.CustomerAggregate.Events; 5 | using Xunit; 6 | 7 | namespace CustomerService.Tests.Domain.CustomerAggregate; 8 | 9 | public class CustomerTests 10 | { 11 | [Fact] 12 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 13 | { 14 | var customer = new Customer(); 15 | 16 | Assert.NotNull(customer); 17 | Assert.IsType(customer); 18 | } 19 | 20 | [Fact] 21 | public void WhenProcessingCreateCustomerCommand_ThenShouldReturnCustomerCreated() 22 | { 23 | var customer = new Customer(); 24 | 25 | var @event = customer.Process(new CreateCustomer(new Customer 26 | { 27 | Id = Guid.NewGuid() 28 | })); 29 | 30 | Assert.NotNull(@event); 31 | Assert.IsAssignableFrom(@event); 32 | } 33 | 34 | [Fact] 35 | public void WhenApplyingCustomerCreatedEvent_ThenShouldHaveIdSet() 36 | { 37 | 38 | var customer = new Customer 39 | { 40 | Id = Guid.NewGuid() 41 | }; 42 | var customerCreated = new CustomerCreated(customer); 43 | 44 | customer.Apply(customerCreated); 45 | 46 | Assert.Equal(customerCreated.EntityId, customer.Id); 47 | } 48 | 49 | [Fact] 50 | public void WhenProcessingUpdateCustomerCommand_ThenShouldReturnCustomerUpdated() 51 | { 52 | var customer = new Customer(); 53 | 54 | var @event = customer.Process(new UpdateCustomer(new Customer 55 | { 56 | Id = Guid.NewGuid() 57 | })); 58 | 59 | Assert.NotNull(@event); 60 | Assert.IsAssignableFrom(@event); 61 | } 62 | 63 | [Fact] 64 | public void WhenApplyingCustomerUpdatedEvent_ThenShouldHaveETagSet() 65 | { 66 | var customer = new Customer(); 67 | var customerUpdated = new CustomerUpdated(customer); 68 | 69 | customer.Apply(customerUpdated); 70 | 71 | Assert.Equal(customerUpdated.EntityETag, customer.ETag); 72 | } 73 | 74 | [Fact] 75 | public void WhenProcessingRemoveCustomerCommand_ThenShouldReturnCustomerRemoved() 76 | { 77 | var customer = new Customer(); 78 | 79 | var @event = customer.Process(new RemoveCustomer(Guid.NewGuid())); 80 | 81 | Assert.NotNull(@event); 82 | Assert.IsAssignableFrom(@event); 83 | } 84 | 85 | [Fact] 86 | public void WhenApplyingCustomerRemovedEvent_ThenShouldBeNotNull() 87 | { 88 | var customer = new Customer(); 89 | var customerRemoved = new CustomerRemoved(Guid.NewGuid()); 90 | 91 | customer.Apply(customerRemoved); 92 | 93 | Assert.NotNull(customer); 94 | } 95 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Domain/CustomerAggregate/QueryHandlers/GetCustomerHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using CustomerService.Domain.CustomerAggregate; 5 | using CustomerService.Domain.CustomerAggregate.Queries; 6 | using CustomerService.Domain.CustomerAggregate.QueryHandlers; 7 | using CustomerService.Repositories; 8 | using CustomerService.Tests.Fakes; 9 | using CustomerService.Tests.Helpers; 10 | using Moq; 11 | using Xunit; 12 | 13 | namespace CustomerService.Tests.Domain.CustomerAggregate.QueryHandlers; 14 | 15 | public class GetCustomerHandlerTests 16 | { 17 | private readonly IMapper _mapper; 18 | private readonly Mock _repositoryMock; 19 | 20 | public GetCustomerHandlerTests() 21 | { 22 | _repositoryMock = new Mock(); 23 | _mapper = MappingHelper.GetMapper(); 24 | } 25 | 26 | [Fact] 27 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 28 | { 29 | var handler = new GetCustomerHandler(_repositoryMock.Object); 30 | 31 | Assert.NotNull(handler); 32 | Assert.IsType(handler); 33 | } 34 | 35 | [Fact] 36 | public async Task WhenRetrievingEntity_ThenEntityShouldBeReturned() 37 | { 38 | var expected = _mapper.Map(Customers.Customer1); 39 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 40 | .ReturnsAsync(expected); 41 | 42 | var handler = new GetCustomerHandler(_repositoryMock.Object); 43 | 44 | var actual = await handler.Handle(new GetCustomer(Customers.Customer1.Id), default); 45 | 46 | Assert.Equal(expected, actual); 47 | } 48 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Domain/CustomerAggregate/QueryHandlers/GetCustomersHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using CustomerService.Domain.CustomerAggregate; 5 | using CustomerService.Domain.CustomerAggregate.Queries; 6 | using CustomerService.Domain.CustomerAggregate.QueryHandlers; 7 | using CustomerService.Repositories; 8 | using CustomerService.Tests.Fakes; 9 | using CustomerService.Tests.Helpers; 10 | using Moq; 11 | using Xunit; 12 | 13 | namespace CustomerService.Tests.Domain.CustomerAggregate.QueryHandlers; 14 | 15 | public class GetCustomersHandlerTests 16 | { 17 | private readonly IMapper _mapper; 18 | private readonly Mock _repositoryMock; 19 | 20 | public GetCustomersHandlerTests() 21 | { 22 | _repositoryMock = new Mock(); 23 | _mapper = MappingHelper.GetMapper(); 24 | } 25 | 26 | [Fact] 27 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 28 | { 29 | var handler = new GetCustomersHandler(_repositoryMock.Object); 30 | 31 | Assert.NotNull(handler); 32 | Assert.IsType(handler); 33 | } 34 | 35 | [Fact] 36 | public async Task WhenRetrievingEntities_ThenAllEntitiesShouldBeReturned() 37 | { 38 | _repositoryMock.Setup(x => x.GetAsync()) 39 | .ReturnsAsync(new List 40 | { 41 | _mapper.Map(Customers.Customer1), 42 | _mapper.Map(Customers.Customer2), 43 | _mapper.Map(Customers.Customer3) 44 | }); 45 | 46 | var handler = new GetCustomersHandler(_repositoryMock.Object); 47 | 48 | var result = await handler.Handle(new GetCustomers(), default); 49 | 50 | Assert.Collection(result, 51 | c => Assert.Equal(CustomerViews.Customer1.Id, c.Id), 52 | c => Assert.Equal(CustomerViews.Customer2.Id, c.Id), 53 | c => Assert.Equal(CustomerViews.Customer3.Id, c.Id)); 54 | } 55 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Fakes/Customers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CustomerService.DTO.Read; 3 | using CustomerService.DTO.Write; 4 | 5 | namespace CustomerService.Tests.Fakes; 6 | 7 | public static class Customers 8 | { 9 | public static Customer Customer1 => new() 10 | { 11 | Id = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 12 | FirstName = "Elon", 13 | LastName = "Musk", 14 | ShippingAddress = new Address 15 | { 16 | Street = "123 This Street", 17 | City = "Freemont", 18 | State = "CA", 19 | Country = "USA", 20 | PostalCode = "90045" 21 | } 22 | }; 23 | 24 | public static Customer Customer2 => new() 25 | { 26 | Id = Guid.Parse("848f5790-3981-4862-bb7e-a8566aa07026"), 27 | FirstName = "Jeff", 28 | LastName = "Bezos", 29 | ShippingAddress = new Address 30 | { 31 | Street = "123 That Street", 32 | City = "Seattle", 33 | State = "WA", 34 | Country = "USA", 35 | PostalCode = "90045" 36 | } 37 | }; 38 | 39 | public static Customer Customer3 => new() 40 | { 41 | Id = Guid.Parse("1c44eea7-400a-4f6f-ab99-5e8c853ea363"), 42 | FirstName = "Mark", 43 | LastName = "Zuckerberg", 44 | ShippingAddress = new Address 45 | { 46 | Street = "123 Other Street", 47 | City = "Palo Alto", 48 | State = "CA", 49 | Country = "USA", 50 | PostalCode = "98765" 51 | } 52 | }; 53 | } 54 | 55 | public static class CustomerViews 56 | { 57 | public static CustomerView Customer1 => new() 58 | { 59 | Id = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 60 | FirstName = "Elon", 61 | LastName = "Musk", 62 | Street = "123 This Street", 63 | City = "Freemont", 64 | State = "CA", 65 | Country = "USA", 66 | PostalCode = "90045" 67 | }; 68 | 69 | public static CustomerView Customer2 => new() 70 | { 71 | Id = Guid.Parse("848f5790-3981-4862-bb7e-a8566aa07026"), 72 | FirstName = "Jeff", 73 | LastName = "Bezos", 74 | Street = "123 That Street", 75 | City = "Seattle", 76 | State = "WA", 77 | Country = "USA", 78 | PostalCode = "90045" 79 | }; 80 | 81 | public static CustomerView Customer3 => new() 82 | { 83 | Id = Guid.Parse("1c44eea7-400a-4f6f-ab99-5e8c853ea363"), 84 | FirstName = "Mark", 85 | LastName = "Zuckerberg", 86 | Street = "123 Other Street", 87 | City = "Palo Alto", 88 | State = "CA", 89 | Country = "USA", 90 | PostalCode = "98765" 91 | }; 92 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Helpers/CustomerComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CustomerService.Domain.CustomerAggregate; 4 | 5 | namespace CustomerService.Tests.Helpers; 6 | 7 | public class CustomerComparer : IEqualityComparer 8 | { 9 | public bool Equals(Customer? x, Customer? y) 10 | { 11 | if (ReferenceEquals(x, y)) return true; 12 | if (ReferenceEquals(x, null)) return false; 13 | if (ReferenceEquals(y, null)) return false; 14 | if (x.GetType() != y.GetType()) return false; 15 | return x.FirstName == y.FirstName && x.LastName == y.LastName && x.ShippingAddress.Equals(y.ShippingAddress); 16 | } 17 | 18 | public int GetHashCode(Customer obj) 19 | { 20 | return HashCode.Combine(obj.FirstName, obj.LastName, obj.ShippingAddress); 21 | } 22 | } -------------------------------------------------------------------------------- /test/CustomerService.Tests/Helpers/MappingHelper.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CustomerService.Mapping; 3 | 4 | namespace CustomerService.Tests.Helpers; 5 | 6 | public static class MappingHelper 7 | { 8 | public static IMapper GetMapper() 9 | { 10 | var config = new MapperConfiguration( 11 | cfg => { cfg.AddProfile(typeof(AutoMapperProfile)); }); 12 | 13 | return config.CreateMapper(); 14 | } 15 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Configuration/ReferenceArchSpecsSettings.cs: -------------------------------------------------------------------------------- 1 | namespace EventDriven.ReferenceArchitecture.Specs.Configuration; 2 | 3 | public class ReferenceArchSpecsSettings 4 | { 5 | public Guid Customer1Id { get; set; } 6 | public Guid Customer2Id { get; set; } 7 | public Guid Customer3Id { get; set; } 8 | public Guid CustomerPubSub1Id { get; set; } 9 | public Guid Order1Id { get; set; } 10 | public Guid Order2Id { get; set; } 11 | public Guid Order3Id { get; set; } 12 | public Guid Order4Id { get; set; } 13 | public Guid OrderPubSub1Id { get; set; } 14 | public Guid OrderPubSub2Id { get; set; } 15 | public string? CustomerServiceBaseAddress { get; set; } 16 | public string? OrderServiceBaseAddress { get; set; } 17 | public TimeSpan AddressUpdateTimeout { get; set; } 18 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/EventDriven.ReferenceArchitecture.Specs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Features/CustomerService.feature: -------------------------------------------------------------------------------- 1 | @notParallel 2 | Feature: Customer Service 3 | Customer Service API 4 | 5 | Scenario: View customers 6 | Given customers have been created with 'customers.json' 7 | When I make a GET request for 'Customer' to 'api/customer' 8 | Then the response status code should be '200' 9 | And the response customers-view should be 'customers-view.json' 10 | 11 | Scenario: View customer 12 | Given a customer has been created with 'customer.json' 13 | When I make a GET request for 'Customer' to 'api/customer/22eea083-6f0d-48f2-8c82-65ac850e5aad' 14 | Then the response status code should be '200' 15 | And the response customer-view should be 'customer-view.json' 16 | 17 | Scenario: Create a customer 18 | When I make a POST request for 'Customer' with 'customer.json' to 'api/customer' 19 | Then the response status code should be '201' 20 | And the location header should be 'api/customer/22eea083-6f0d-48f2-8c82-65ac850e5aad' 21 | And the response customer should be 'customer.json' 22 | 23 | Scenario: Update a customer 24 | Given a customer has been created with 'customer.json' 25 | When I make a PUT request for 'Customer' with 'updated-customer.json' to 'api/customer' 26 | Then the response status code should be '200' 27 | And the response customer should be 'updated-customer.json' 28 | 29 | Scenario: Remove a customer 30 | Given a customer has been created with 'customer.json' 31 | When I make a DELETE request for 'Customer' with id '3fa85f64-5717-4562-b3fc-2c963f66afa6' to 'api/customer' 32 | Then the response status code should be '204' 33 | -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Features/OrderService.feature: -------------------------------------------------------------------------------- 1 | @notParallel 2 | Feature: Order Service 3 | Order Service API 4 | 5 | Scenario: View orders 6 | Given orders have been created with 'orders.json' 7 | When I make a GET request for 'Order' to 'api/order' 8 | Then the response status code should be '200' 9 | And the response orders-view should be 'orders-view.json' 10 | 11 | Scenario: View orders by customer 12 | Given orders have been created with 'orders.json' 13 | When I make a GET request for 'Order' to 'api/order/customer/22eea083-6f0d-48f2-8c82-65ac850e5aad' 14 | Then the response status code should be '200' 15 | And the response orders-view should be 'orders-view.json' 16 | 17 | Scenario: View order 18 | Given orders have been created with 'orders.json' 19 | When I make a GET request for 'Order' to 'api/order/3fa85f64-5717-4562-b3fc-2c963f66afa6' 20 | Then the response status code should be '200' 21 | And the response customer-view should be 'order-view.json' 22 | 23 | Scenario: Create an order 24 | When I make a POST request for 'Order' with 'order.json' to 'api/order' 25 | Then the response status code should be '201' 26 | And the location header should be 'api/order/3fa85f64-5717-4562-b3fc-2c963f66afa6' 27 | And the response customer should be 'order.json' 28 | 29 | Scenario: Update an order 30 | Given an order has been created with 'order.json' 31 | When I make a PUT request for 'Order' with 'updated-order.json' to 'api/order' 32 | Then the response status code should be '200' 33 | And the response customer should be 'updated-order.json' 34 | 35 | Scenario: Ship an order 36 | Given an order has been created with 'order-to-ship.json' 37 | When I make a PUT request for 'Order' with the following data to 'api/order/ship' 38 | | Id | ETag | 39 | | dd798647-9c83-4d8f-8102-4d70d0c6c4c3 | 4a0f4ae5-c304-4a6a-8d46-efc8e5af5218 | 40 | Then the response status code should be '200' 41 | And the response customer should be 'shipped-order.json' 42 | 43 | Scenario: Cancel an order 44 | Given an order has been created with 'order-to-cancel.json' 45 | When I make a PUT request for 'Order' with the following data to 'api/order/cancel' 46 | | Id | ETag | 47 | | 775c520f-2fec-4ffd-b5fb-870e605fd05b | 4a0f4ae5-c304-4a6a-8d46-efc8e5af5218 | 48 | Then the response status code should be '200' 49 | And the response customer should be 'cancelled-order.json' 50 | 51 | Scenario: Remove an order 52 | Given an order has been created with 'order.json' 53 | When I make a DELETE request for 'Order' with id '3fa85f64-5717-4562-b3fc-2c963f66afa6' to 'api/order' 54 | Then the response status code should be '204' 55 | -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Features/PublishSubscribe.feature: -------------------------------------------------------------------------------- 1 | @notParallel 2 | Feature: Publish Subscribe 3 | Data propagation between services over an event bus abstraction layer. 4 | 5 | Scenario: Publish customer address updated event 6 | Given a customer has been created with 'customer-pubsub.json' 7 | And orders have been created with 'orders-pubsub.json' 8 | When I make a PUT request for 'Customer' with 'updated-customer-pubsub.json' to 'api/customer' 9 | Then the response status code should be '200' 10 | And the address for orders should equal 'updated-address-pubsub.json' 11 | -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Features/PublishSubscribe.feature.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by SpecFlow (https://www.specflow.org/). 4 | // SpecFlow Version:3.9.0.0 5 | // SpecFlow Generator Version:3.9.0.0 6 | // 7 | // Changes to this file may cause incorrect behavior and will be lost if 8 | // the code is regenerated. 9 | // 10 | // ------------------------------------------------------------------------------ 11 | #region Designer generated code 12 | #pragma warning disable 13 | namespace EventDriven.ReferenceArchitecture.Specs.Features 14 | { 15 | using TechTalk.SpecFlow; 16 | using System; 17 | using System.Linq; 18 | 19 | 20 | [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] 21 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 22 | [Xunit.CollectionAttribute("SpecFlowNonParallelizableFeatures")] 23 | [Xunit.TraitAttribute("Category", "notParallel")] 24 | public partial class PublishSubscribeFeature : object, Xunit.IClassFixture, System.IDisposable 25 | { 26 | 27 | private static TechTalk.SpecFlow.ITestRunner testRunner; 28 | 29 | private static string[] featureTags = new string[] { 30 | "notParallel"}; 31 | 32 | private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; 33 | 34 | #line 1 "PublishSubscribe.feature" 35 | #line hidden 36 | 37 | public PublishSubscribeFeature(PublishSubscribeFeature.FixtureData fixtureData, EventDriven_ReferenceArchitecture_Specs_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) 38 | { 39 | this._testOutputHelper = testOutputHelper; 40 | this.TestInitialize(); 41 | } 42 | 43 | public static void FeatureSetup() 44 | { 45 | testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); 46 | TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Publish Subscribe", "\tData propagation between services over an event bus abstraction layer.", ProgrammingLanguage.CSharp, featureTags); 47 | testRunner.OnFeatureStart(featureInfo); 48 | } 49 | 50 | public static void FeatureTearDown() 51 | { 52 | testRunner.OnFeatureEnd(); 53 | testRunner = null; 54 | } 55 | 56 | public void TestInitialize() 57 | { 58 | } 59 | 60 | public void TestTearDown() 61 | { 62 | testRunner.OnScenarioEnd(); 63 | } 64 | 65 | public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) 66 | { 67 | testRunner.OnScenarioInitialize(scenarioInfo); 68 | testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testOutputHelper); 69 | } 70 | 71 | public void ScenarioStart() 72 | { 73 | testRunner.OnScenarioStart(); 74 | } 75 | 76 | public void ScenarioCleanup() 77 | { 78 | testRunner.CollectScenarioErrors(); 79 | } 80 | 81 | void System.IDisposable.Dispose() 82 | { 83 | this.TestTearDown(); 84 | } 85 | 86 | [Xunit.SkippableFactAttribute(DisplayName="Publish customer address updated event")] 87 | [Xunit.TraitAttribute("FeatureTitle", "Publish Subscribe")] 88 | [Xunit.TraitAttribute("Description", "Publish customer address updated event")] 89 | public void PublishCustomerAddressUpdatedEvent() 90 | { 91 | string[] tagsOfScenario = ((string[])(null)); 92 | System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); 93 | TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish customer address updated event", null, tagsOfScenario, argumentsOfScenario, featureTags); 94 | #line 5 95 | this.ScenarioInitialize(scenarioInfo); 96 | #line hidden 97 | if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) 98 | { 99 | testRunner.SkipScenario(); 100 | } 101 | else 102 | { 103 | this.ScenarioStart(); 104 | #line 6 105 | testRunner.Given("a customer has been created with \'customer-pubsub.json\'", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); 106 | #line hidden 107 | #line 7 108 | testRunner.And("orders have been created with \'orders-pubsub.json\'", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); 109 | #line hidden 110 | #line 8 111 | testRunner.When("I make a PUT request for \'Customer\' with \'updated-customer-pubsub.json\' to \'api/c" + 112 | "ustomer\'", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); 113 | #line hidden 114 | #line 9 115 | testRunner.Then("the response status code should be \'200\'", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); 116 | #line hidden 117 | #line 10 118 | testRunner.And("the address for orders should equal \'updated-address-pubsub.json\'", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); 119 | #line hidden 120 | } 121 | this.ScenarioCleanup(); 122 | } 123 | 124 | [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] 125 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 126 | public class FixtureData : System.IDisposable 127 | { 128 | 129 | public FixtureData() 130 | { 131 | PublishSubscribeFeature.FeatureSetup(); 132 | } 133 | 134 | void System.IDisposable.Dispose() 135 | { 136 | PublishSubscribeFeature.FeatureTearDown(); 137 | } 138 | } 139 | } 140 | } 141 | #pragma warning restore 142 | #endregion 143 | -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/CustomerReadDtoComparer.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.DTO.Read; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public class CustomerReadDtoComparer : IEqualityComparer 6 | { 7 | public bool Equals(CustomerView? x, CustomerView? y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.Id == y.Id 14 | && x.FirstName == y.FirstName 15 | && x.LastName == y.LastName 16 | && x.Street == y.Street 17 | && x.City == y.City 18 | && x.State == y.State 19 | && x.Country == y.Country 20 | && x.PostalCode == y.PostalCode; 21 | } 22 | 23 | public int GetHashCode(CustomerView obj) => 24 | HashCode.Combine(obj.FirstName, obj.LastName, obj.Street, obj.City, obj.State, obj.Country, obj.PostalCode); 25 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/CustomerWriteDtoAddressComparer.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.DTO.Write; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public class CustomerWriteDtoAddressComparer : IEqualityComparer
6 | { 7 | public bool Equals(Address? x, Address? y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.Street == y.Street 14 | && x.City == y.City 15 | && x.Country == y.Country 16 | && x.PostalCode == y.PostalCode; 17 | } 18 | 19 | public int GetHashCode(Address obj) => 20 | HashCode.Combine(obj.Street, obj.City, obj.Country, obj.State, obj.PostalCode); 21 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/CustomerWriteDtoComparer.cs: -------------------------------------------------------------------------------- 1 | using CustomerService.DTO.Write; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public class CustomerWriteDtoComparer : IEqualityComparer 6 | { 7 | public bool Equals(Customer? x, Customer? y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.Id == y.Id 14 | && x.FirstName == y.FirstName 15 | && x.LastName == y.LastName 16 | && new CustomerWriteDtoAddressComparer().Equals(x.ShippingAddress, y.ShippingAddress); 17 | } 18 | 19 | public int GetHashCode(Customer obj) => 20 | HashCode.Combine(obj.FirstName, obj.LastName, obj.ShippingAddress); 21 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public static class JsonHelper 6 | { 7 | public static string JsonPrettify(this string json) 8 | { 9 | using var jDoc = JsonDocument.Parse(json); 10 | return JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true }); 11 | } 12 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/MappingHelper.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public static class MappingHelper 6 | { 7 | public static IMapper GetMapper() 8 | where TMappingProfile : Profile 9 | { 10 | var config = new MapperConfiguration( 11 | cfg => { cfg.AddProfile(typeof(TMappingProfile)); }); 12 | 13 | return config.CreateMapper(); 14 | } 15 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/OrderReadDtoComparer.cs: -------------------------------------------------------------------------------- 1 | using OrderService.DTO.Read; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public class OrderReadDtoComparer : IEqualityComparer 6 | { 7 | public bool Equals(OrderView? x, OrderView? y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.Id == y.Id 14 | && x.CustomerId == y.CustomerId 15 | && x.OrderDate == y.OrderDate 16 | && x.OrderTotal == y.OrderTotal 17 | && x.Street == y.Street 18 | && x.City == y.City 19 | && x.State == y.State 20 | && x.Country == y.Country 21 | && x.PostalCode == y.PostalCode 22 | && x.OrderDate == y.OrderDate; 23 | } 24 | 25 | public int GetHashCode(OrderView obj) => 26 | HashCode.Combine(obj.CustomerId, obj.OrderDate, obj.OrderTotal, 27 | obj.Street, obj.City, obj.State, obj.Country, obj.PostalCode); 28 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/OrderWriteDtoAddressComparer.cs: -------------------------------------------------------------------------------- 1 | using OrderService.DTO.Write; 2 | 3 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 4 | 5 | public class OrderWriteDtoAddressComparer : IEqualityComparer
6 | { 7 | public bool Equals(Address? x, Address? y) 8 | { 9 | if (ReferenceEquals(x, y)) return true; 10 | if (ReferenceEquals(x, null)) return false; 11 | if (ReferenceEquals(y, null)) return false; 12 | if (x.GetType() != y.GetType()) return false; 13 | return x.Street == y.Street 14 | && x.City == y.City 15 | && x.Country == y.Country 16 | && x.PostalCode == y.PostalCode; 17 | } 18 | 19 | public int GetHashCode(Address obj) => 20 | HashCode.Combine(obj.Street, obj.City, obj.Country, obj.State, obj.PostalCode); 21 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Helpers/PutRequest.cs: -------------------------------------------------------------------------------- 1 | namespace EventDriven.ReferenceArchitecture.Specs.Helpers; 2 | 3 | public class PutRequest 4 | { 5 | public Guid Id { get; set; } 6 | public string ETag { get; set; } = null!; 7 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Hooks/Hook.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using BoDi; 3 | using CustomerService.Configuration; 4 | using CustomerService.Domain.CustomerAggregate; 5 | using CustomerService.Repositories; 6 | using EventDriven.DependencyInjection; 7 | using EventDriven.DependencyInjection.URF.Mongo; 8 | using EventDriven.ReferenceArchitecture.Specs.Configuration; 9 | using EventDriven.ReferenceArchitecture.Specs.Repositories; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using OrderService.Configuration; 14 | using OrderService.Domain.OrderAggregate; 15 | using OrderService.Repositories; 16 | 17 | namespace EventDriven.ReferenceArchitecture.Specs.Hooks; 18 | 19 | [Binding] 20 | public class Hooks(IObjectContainer objectContainer) 21 | { 22 | [BeforeScenario] 23 | public async Task RegisterServices() 24 | { 25 | var host = Host 26 | .CreateDefaultBuilder() 27 | .ConfigureServices(services => 28 | { 29 | var config = services.BuildServiceProvider() 30 | .GetRequiredService(); 31 | services.AddAppSettings(config); 32 | services.AddHttpClient(); 33 | services.AddSingleton(); 34 | services.AddSingleton(); 35 | services.AddMongoDbSettings(config); 36 | services.AddMongoDbSettings(config); 37 | }) 38 | .Build(); 39 | 40 | var settings = host.Services.GetRequiredService(); 41 | var customerRepository = host.Services.GetRequiredService(); 42 | var orderRepository = host.Services.GetRequiredService(); 43 | var httpClientFactory = host.Services.GetRequiredService(); 44 | 45 | await ClearData(customerRepository, settings.Customer1Id); 46 | await ClearData(customerRepository, settings.Customer2Id); 47 | await ClearData(customerRepository, settings.Customer3Id); 48 | await ClearData(customerRepository, settings.CustomerPubSub1Id); 49 | await ClearData(orderRepository, settings.Order1Id); 50 | await ClearData(orderRepository, settings.Order2Id); 51 | await ClearData(orderRepository, settings.Order3Id); 52 | await ClearData(orderRepository, settings.Order4Id); 53 | await ClearData(orderRepository, settings.OrderPubSub1Id); 54 | await ClearData(orderRepository, settings.OrderPubSub2Id); 55 | 56 | objectContainer.RegisterInstanceAs(settings); 57 | objectContainer.RegisterInstanceAs(httpClientFactory); 58 | objectContainer.RegisterInstanceAs(new JsonFilesRepository()); 59 | objectContainer.RegisterInstanceAs(customerRepository); 60 | objectContainer.RegisterInstanceAs(orderRepository); 61 | } 62 | 63 | private async Task ClearData(TRepository repository, Guid entityId) 64 | { 65 | if (repository is ICustomerRepository customerRepository) 66 | await customerRepository.RemoveAsync(entityId); 67 | if (repository is IOrderRepository orderRepository) 68 | await orderRepository.RemoveAsync(entityId); 69 | } 70 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Reference Architecture: User Acceptance Tests 2 | 3 | SpecFlow tests for EventDriven.ReferenceArchitecture. 4 | 5 | ## Prerequisites 6 | - [.NET Core SDK](https://dotnet.microsoft.com/download) (6.0 or greater) 7 | - [Docker Desktop](https://www.docker.com/products/docker-desktop) 8 | - MongoDB Docker: `docker run --name mongo -d -p 27017:27017 -v /tmp/mongo/data:/data/db mongo` 9 | - [MongoDB Client](https://robomongo.org/) 10 | - Download Studio 3T. 11 | - Add connection to localhost on port 27017. 12 | - [Dapr](https://dapr.io/) (Distributed Application Runtime) 13 | - [Install Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) 14 | - [Initialize Dapr](https://docs.dapr.io/getting-started/install-dapr-selfhost/) 15 | - [.NET Aspire Workload](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/setup-tooling?tabs=dotnet-cli#install-net-aspire) 16 | - [Specflow](https://specflow.org/) IDE Plugin (recommended) 17 | - [Visual Studio](https://docs.specflow.org/projects/getting-started/en/latest/GettingStarted/Step1.html) 18 | - [JetBrains Rider](https://docs.specflow.org/projects/specflow/en/latest/Rider/rider-installation.html) 19 | 20 | ## Usage 21 | 22 | 1. Using an IDE such as Visual Studio or Rider, run the **specs** profile of **ReferenceArchitecture.AppHost**. 23 | 2. Run **EventDriven.ReferenceArchitecture.Specs** from the Test explorer of your IDE. 24 | 3. Alternatively, open a terminal at **EventDriven.ReferenceArchitecture.Specs**, then run `dotnet test` 25 | 26 | -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/Repositories/JsonFilesRepository.cs: -------------------------------------------------------------------------------- 1 | namespace EventDriven.ReferenceArchitecture.Specs.Repositories; 2 | 3 | public class JsonFilesRepository 4 | { 5 | private const string Root = "../../../json/"; 6 | public Dictionary Files { get; } = new(); 7 | 8 | public JsonFilesRepository(params string[] files) 9 | { 10 | var filesList = files.ToList(); 11 | if (!filesList.Any()) 12 | foreach (var file in Directory.GetFiles(Root)) 13 | filesList.Add(Path.GetFileName(file)); 14 | 15 | foreach (var file in filesList) 16 | { 17 | var path = Path.Combine(Root, file); 18 | var contents = File.ReadAllText(path); 19 | Files.Add(file, contents); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Debug" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ReferenceArchSpecsSettings": { 10 | "Customer1Id": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 11 | "Customer2Id": "848f5790-3981-4862-bb7e-a8566aa07026", 12 | "Customer3Id": "ff15b0aa-3a5a-4ef8-a556-e0248a4a820e", 13 | "CustomerPubSub1Id": "1c44eea7-400a-4f6f-ab99-5e8c853ea363", 14 | "Order1Id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 15 | "Order2Id": "fd06384d-24cd-4a7f-a5f5-b05ec683bfdd", 16 | "Order3Id": "dd798647-9c83-4d8f-8102-4d70d0c6c4c3", 17 | "Order4Id": "775c520f-2fec-4ffd-b5fb-870e605fd05b", 18 | "OrderPubSub1Id": "94de0310-7a0d-4aff-8a47-5d2a60575e5e", 19 | "OrderPubSub2Id": "a9d6288a-07e8-4df8-8a32-85d7483a7f6f", 20 | "CustomerServiceBaseAddress": "https://localhost:5656/", 21 | "OrderServiceBaseAddress": "https://localhost:5757/", 22 | "AddressUpdateTimeout" : "00:00:02" 23 | }, 24 | "CustomerDatabaseSettings": { 25 | "ConnectionString": "mongodb://localhost:27017", 26 | "DatabaseName": "CustomersTestDb", 27 | "CollectionName": "Customers" 28 | }, 29 | "OrderDatabaseSettings": { 30 | "ConnectionString": "mongodb://localhost:27017", 31 | "DatabaseName": "OrdersTestDb", 32 | "CollectionName": "Orders" 33 | } 34 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/cancelled-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "775c520f-2fec-4ffd-b5fb-870e605fd05b", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderItems": [ 6 | { 7 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 8 | "productName": "Espresso", 9 | "productPrice": 2.50 10 | }, 11 | { 12 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 13 | "productName": "Cappuccino", 14 | "productPrice": 3.50 15 | } 16 | ], 17 | "shippingAddress": { 18 | "street": "123 This Street", 19 | "city": "Freemont", 20 | "state": "CA", 21 | "country": "USA", 22 | "postalCode": "90045" 23 | }, 24 | "orderState": 2, 25 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 26 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/customer-pubsub.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1c44eea7-400a-4f6f-ab99-5e8c853ea363", 3 | "eTag": "d3c97e56-ad93-471f-b652-2d793e513eaf", 4 | "firstName": "Mark", 5 | "lastName": "Zuckerberg", 6 | "shippingAddress": { 7 | "street": "123 Other Street", 8 | "city": "Palo Alto", 9 | "state": "CA", 10 | "country": "USA", 11 | "postalCode": "98765" 12 | } 13 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/customer-view.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 3 | "eTag": "d82a8984-2cb6-4143-8c3a-ffd3ec907b9c", 4 | "firstName": "Elon", 5 | "lastName": "Musk", 6 | "street": "123 This Street", 7 | "city": "Freemont", 8 | "state": "CA", 9 | "country": "USA", 10 | "postalCode": "90045" 11 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 3 | "eTag": "d82a8984-2cb6-4143-8c3a-ffd3ec907b9c", 4 | "firstName": "Elon", 5 | "lastName": "Musk", 6 | "shippingAddress": { 7 | "street": "123 This Street", 8 | "city": "Freemont", 9 | "state": "CA", 10 | "country": "USA", 11 | "postalCode": "90045" 12 | } 13 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/customers-view.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "ff15b0aa-3a5a-4ef8-a556-e0248a4a820e", 4 | "eTag": "a50d5b3e-75ce-4142-ac15-20fc42c1bd98", 5 | "firstName": "Bill", 6 | "lastName": "Gates", 7 | "street": "123 Another Street", 8 | "city": "Redmond", 9 | "state": "WA", 10 | "country": "USA", 11 | "postalCode": "98008" 12 | }, 13 | { 14 | "id": "848f5790-3981-4862-bb7e-a8566aa07026", 15 | "eTag": "c3af610e-a86c-4f43-ac39-73de9d7b5cd6", 16 | "firstName": "Jeff", 17 | "lastName": "Bezos", 18 | "street": "123 That Street", 19 | "city": "Seattle", 20 | "state": "WA", 21 | "country": "USA", 22 | "postalCode": "54321" 23 | } 24 | ] -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/customers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "ff15b0aa-3a5a-4ef8-a556-e0248a4a820e", 4 | "eTag": "a50d5b3e-75ce-4142-ac15-20fc42c1bd98", 5 | "firstName": "Bill", 6 | "lastName": "Gates", 7 | "shippingAddress": { 8 | "street": "123 Another Street", 9 | "city": "Redmond", 10 | "state": "WA", 11 | "country": "USA", 12 | "postalCode": "98008" 13 | } 14 | }, 15 | { 16 | "id": "848f5790-3981-4862-bb7e-a8566aa07026", 17 | "eTag": "c3af610e-a86c-4f43-ac39-73de9d7b5cd6", 18 | "firstName": "Jeff", 19 | "lastName": "Bezos", 20 | "shippingAddress": { 21 | "street": "123 That Street", 22 | "city": "Seattle", 23 | "state": "WA", 24 | "country": "USA", 25 | "postalCode": "54321" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/order-to-cancel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "775c520f-2fec-4ffd-b5fb-870e605fd05b", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderItems": [ 6 | { 7 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 8 | "productName": "Espresso", 9 | "productPrice": 2.50 10 | }, 11 | { 12 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 13 | "productName": "Cappuccino", 14 | "productPrice": 3.50 15 | } 16 | ], 17 | "shippingAddress": { 18 | "street": "123 This Street", 19 | "city": "Freemont", 20 | "state": "CA", 21 | "country": "USA", 22 | "postalCode": "90045" 23 | }, 24 | "orderState": 0, 25 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 26 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/order-to-ship.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dd798647-9c83-4d8f-8102-4d70d0c6c4c3", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderItems": [ 6 | { 7 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 8 | "productName": "Espresso", 9 | "productPrice": 2.50 10 | }, 11 | { 12 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 13 | "productName": "Cappuccino", 14 | "productPrice": 3.50 15 | } 16 | ], 17 | "shippingAddress": { 18 | "street": "123 This Street", 19 | "city": "Freemont", 20 | "state": "CA", 21 | "country": "USA", 22 | "postalCode": "90045" 23 | }, 24 | "orderState": 0, 25 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 26 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/order-view.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderTotal": 6, 6 | "street": "123 This Street", 7 | "city": "Freemont", 8 | "state": "CA", 9 | "country": "USA", 10 | "postalCode": "90045", 11 | "orderState": 0, 12 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 13 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/order.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderItems": [ 6 | { 7 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 8 | "productName": "Espresso", 9 | "productPrice": 2.50 10 | }, 11 | { 12 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 13 | "productName": "Cappuccino", 14 | "productPrice": 3.50 15 | } 16 | ], 17 | "shippingAddress": { 18 | "street": "123 This Street", 19 | "city": "Freemont", 20 | "state": "CA", 21 | "country": "USA", 22 | "postalCode": "90045" 23 | }, 24 | "orderState": 0, 25 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 26 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/orders-pubsub.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "94de0310-7a0d-4aff-8a47-5d2a60575e5e", 4 | "customerId": "1c44eea7-400a-4f6f-ab99-5e8c853ea363", 5 | "orderDate": "2021-04-15T22:54:44.485Z", 6 | "orderItems": [ 7 | { 8 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 9 | "productName": "Espresso", 10 | "productPrice": 2.50 11 | }, 12 | { 13 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 14 | "productName": "Cappuccino", 15 | "productPrice": 3.50 16 | } 17 | ], 18 | "shippingAddress": { 19 | "street": "123 Other Street", 20 | "city": "Palo Alto", 21 | "state": "CA", 22 | "country": "USA", 23 | "postalCode": "98765" 24 | }, 25 | "orderState": 0, 26 | "eTag": "32c838de-bcb6-4518-bf4e-13eb09edf280" 27 | }, 28 | { 29 | "id": "a9d6288a-07e8-4df8-8a32-85d7483a7f6f", 30 | "customerId": "1c44eea7-400a-4f6f-ab99-5e8c853ea363", 31 | "orderDate": "2021-04-15T22:54:44.485Z", 32 | "orderItems": [ 33 | { 34 | "productId": "42b27777-6eff-4193-828d-5fe554a705d1", 35 | "productName": "Chai", 36 | "productPrice": 1.50 37 | } 38 | ], 39 | "shippingAddress": { 40 | "street": "123 Other Street", 41 | "city": "Palo Alto", 42 | "state": "CA", 43 | "country": "USA", 44 | "postalCode": "98765" 45 | }, 46 | "orderState": 0, 47 | "eTag": "9d01bc8c-b354-4b65-88b7-8116b4537c93" 48 | } 49 | ] -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/orders-view.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 4 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 5 | "orderDate": "2021-04-15T22:54:44.485Z", 6 | "orderTotal": 6, 7 | "street": "123 This Street", 8 | "city": "Freemont", 9 | "state": "CA", 10 | "country": "USA", 11 | "postalCode": "90045", 12 | "orderState": 0, 13 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 14 | }, 15 | { 16 | "id": "fd06384d-24cd-4a7f-a5f5-b05ec683bfdd", 17 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 18 | "orderDate": "2021-04-15T22:54:44.485Z", 19 | "orderTotal": 1.5, 20 | "street": "123 This Street", 21 | "city": "Freemont", 22 | "state": "CA", 23 | "country": "USA", 24 | "postalCode": "90045", 25 | "orderState": 0, 26 | "eTag": "39a72ea5-d7f8-4b35-b3e3-d4bf9e6879dd" 27 | } 28 | ] -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/orders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 4 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 5 | "orderDate": "2021-04-15T22:54:44.485Z", 6 | "orderItems": [ 7 | { 8 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 9 | "productName": "Espresso", 10 | "productPrice": 2.50 11 | }, 12 | { 13 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 14 | "productName": "Cappuccino", 15 | "productPrice": 3.50 16 | } 17 | ], 18 | "shippingAddress": { 19 | "street": "123 This Street", 20 | "city": "Freemont", 21 | "state": "CA", 22 | "country": "USA", 23 | "postalCode": "90045" 24 | }, 25 | "orderState": 0, 26 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 27 | }, 28 | { 29 | "id": "fd06384d-24cd-4a7f-a5f5-b05ec683bfdd", 30 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 31 | "orderDate": "2021-04-15T22:54:44.485Z", 32 | "orderItems": [ 33 | { 34 | "productId": "42b27777-6eff-4193-828d-5fe554a705d1", 35 | "productName": "Chai", 36 | "productPrice": 1.50 37 | } 38 | ], 39 | "shippingAddress": { 40 | "street": "123 This Street", 41 | "city": "Freemont", 42 | "state": "CA", 43 | "country": "USA", 44 | "postalCode": "90045" 45 | }, 46 | "orderState": 0, 47 | "eTag": "39a72ea5-d7f8-4b35-b3e3-d4bf9e6879dd" 48 | } 49 | ] -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/shipped-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dd798647-9c83-4d8f-8102-4d70d0c6c4c3", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderItems": [ 6 | { 7 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 8 | "productName": "Espresso", 9 | "productPrice": 2.50 10 | }, 11 | { 12 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 13 | "productName": "Cappuccino", 14 | "productPrice": 3.50 15 | } 16 | ], 17 | "shippingAddress": { 18 | "street": "123 This Street", 19 | "city": "Freemont", 20 | "state": "CA", 21 | "country": "USA", 22 | "postalCode": "90045" 23 | }, 24 | "orderState": 1, 25 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 26 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/updated-address-pubsub.json: -------------------------------------------------------------------------------- 1 | { 2 | "street": "123 Other Street", 3 | "city": "Los Angeles", 4 | "state": "CA", 5 | "country": "USA", 6 | "postalCode": "90045" 7 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/updated-customer-pubsub.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1c44eea7-400a-4f6f-ab99-5e8c853ea363", 3 | "eTag": "d3c97e56-ad93-471f-b652-2d793e513eaf", 4 | "firstName": "Mark", 5 | "lastName": "Zuckerberg", 6 | "shippingAddress": { 7 | "street": "123 Other Street", 8 | "city": "Los Angeles", 9 | "state": "CA", 10 | "country": "USA", 11 | "postalCode": "90045" 12 | } 13 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/updated-customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 3 | "eTag": "d82a8984-2cb6-4143-8c3a-ffd3ec907b9c", 4 | "firstName": "Kimbal", 5 | "lastName": "Musk", 6 | "shippingAddress": { 7 | "street": "123 This Street", 8 | "city": "Freemont", 9 | "state": "CA", 10 | "country": "USA", 11 | "postalCode": "90045" 12 | } 13 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/json/updated-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 3 | "customerId": "22eea083-6f0d-48f2-8c82-65ac850e5aad", 4 | "orderDate": "2021-04-15T22:54:44.485Z", 5 | "orderItems": [ 6 | { 7 | "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", 8 | "productName": "Americano", 9 | "productPrice": 4.50 10 | }, 11 | { 12 | "productId": "5a8c5f7f-0781-4d5e-9d9b-41ef2da5f3c3", 13 | "productName": "Macchiato", 14 | "productPrice": 5.50 15 | } 16 | ], 17 | "shippingAddress": { 18 | "street": "123 This Street", 19 | "city": "Freemont", 20 | "state": "CA", 21 | "country": "USA", 22 | "postalCode": "90045" 23 | }, 24 | "orderState": 0, 25 | "eTag": "4a0f4ae5-c304-4a6a-8d46-efc8e5af5218" 26 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/specflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator": { 3 | "addNonParallelizableMarkerForTags": [ "notParallel" ] 4 | } 5 | } -------------------------------------------------------------------------------- /test/EventDriven.ReferenceArchitecture.Specs/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "parallelizeTestCollections": false 3 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Controllers/OrderQueryControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using AutoMapper; 5 | using EventDriven.CQRS.Abstractions.Queries; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Moq; 8 | using OrderService.Controllers; 9 | using OrderService.Domain.OrderAggregate; 10 | using OrderService.Domain.OrderAggregate.Queries; 11 | using OrderService.DTO.Read; 12 | using OrderService.Tests.Fakes; 13 | using OrderService.Tests.Helpers; 14 | using Xunit; 15 | 16 | namespace OrderService.Tests.Controllers; 17 | 18 | public class OrderQueryControllerTests 19 | { 20 | private readonly IMapper _mapper; 21 | private readonly Mock _queryBrokerMoq; 22 | 23 | public OrderQueryControllerTests() 24 | { 25 | _queryBrokerMoq = new Mock(); 26 | _mapper = MappingHelper.GetMapper(); 27 | } 28 | 29 | [Fact] 30 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 31 | { 32 | var controller = new OrderQueryController(_queryBrokerMoq.Object); 33 | 34 | Assert.NotNull(controller); 35 | Assert.IsAssignableFrom(controller); 36 | Assert.IsType(controller); 37 | } 38 | 39 | [Fact] 40 | public async Task WhenRetrievingAllOrders_ThenAllOrdersShouldBeReturned() 41 | { 42 | _queryBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 43 | .ReturnsAsync(new List 44 | { 45 | _mapper.Map(Orders.Order1), 46 | _mapper.Map(Orders.Order2) 47 | }); 48 | var controller = new OrderQueryController(_queryBrokerMoq.Object); 49 | 50 | var actionResult = await controller.GetOrders(); 51 | var okResult = Assert.IsType(actionResult); 52 | var value = (IEnumerable) okResult.Value!; 53 | 54 | Assert.Collection(value, 55 | x => Assert.Equal(OrderViews.Order1.Id, x.Id), 56 | x => Assert.Equal(OrderViews.Order2.Id, x.Id)); 57 | } 58 | 59 | [Fact] 60 | public async Task WhenRetrievingAllOrdersForACustomer_ThenAllOrdersShouldBeReturned() 61 | { 62 | _queryBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 63 | .ReturnsAsync(new List 64 | { 65 | _mapper.Map(Orders.Order1), 66 | _mapper.Map(Orders.Order2) 67 | }); 68 | var controller = new OrderQueryController(_queryBrokerMoq.Object); 69 | 70 | var actionResult = await controller.GetOrders(Guid.NewGuid()); 71 | var okResult = Assert.IsType(actionResult); 72 | var value = (IEnumerable) okResult.Value!; 73 | 74 | Assert.Collection(value, 75 | x => Assert.Equal(OrderViews.Order1.Id, x.Id), 76 | x => Assert.Equal(OrderViews.Order2.Id, x.Id)); 77 | } 78 | 79 | [Fact] 80 | public async Task WhenRetrievingAnOrderById_ThenShouldReturnOrder() 81 | { 82 | _queryBrokerMoq.Setup(x => x.SendAsync(It.IsAny())) 83 | .ReturnsAsync(_mapper.Map(Orders.Order1)); 84 | var controller = new OrderQueryController(_queryBrokerMoq.Object); 85 | 86 | var actionResult = await controller.GetOrder(Guid.NewGuid()); 87 | var okResult = Assert.IsType(actionResult); 88 | var value = (OrderView) okResult.Value!; 89 | 90 | Assert.Equal(OrderViews.Order1.Id, value.Id); 91 | } 92 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/CommandHandlers/CancelOrderHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using EventDriven.CQRS.Abstractions.Commands; 5 | using EventDriven.DDD.Abstractions.Repositories; 6 | using Moq; 7 | using OrderService.Domain.OrderAggregate; 8 | using OrderService.Domain.OrderAggregate.Commands; 9 | using OrderService.Domain.OrderAggregate.CommandHandlers; 10 | using OrderService.Repositories; 11 | using OrderService.Tests.Fakes; 12 | using OrderService.Tests.Helpers; 13 | using Xunit; 14 | 15 | namespace OrderService.Tests.Domain.OrderAggregate.CommandHandlers; 16 | 17 | public class CancelOrderHandlerTests 18 | { 19 | private readonly IMapper _mapper; 20 | 21 | private readonly Mock _repositoryMoq; 22 | 23 | public CancelOrderHandlerTests() 24 | { 25 | _repositoryMoq = new Mock(); 26 | _mapper = MappingHelper.GetMapper(); 27 | } 28 | 29 | [Fact] 30 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 31 | { 32 | var handler = new CancelOrderHandler(_repositoryMoq.Object); 33 | 34 | Assert.NotNull(handler); 35 | Assert.IsType(handler); 36 | } 37 | 38 | [Fact] 39 | public async Task WhenOrderDoesNotExist_ThenShouldReturnNotFound() 40 | { 41 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 42 | .ReturnsAsync((Order) null!); 43 | var handler = new CancelOrderHandler(_repositoryMoq.Object); 44 | 45 | var result = await handler.Handle(new CancelOrder(Guid.Empty), default); 46 | 47 | Assert.NotNull(result); 48 | Assert.Equal(CommandOutcome.NotFound, result.Outcome); 49 | } 50 | 51 | [Fact] 52 | public async Task WhenOrderIsCancelled_ThenResultingOrderStateShouldBeSetToCancelled() 53 | { 54 | var order = _mapper.Map(Orders.Order1); 55 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 56 | .ReturnsAsync(order); 57 | _repositoryMoq.Setup(x => x.UpdateOrderStateAsync(It.IsAny(), It.IsAny())) 58 | .ReturnsAsync(order); 59 | var handler = new CancelOrderHandler(_repositoryMoq.Object); 60 | 61 | var result = await handler.Handle(new CancelOrder(Guid.Empty), default); 62 | 63 | Assert.NotNull(result); 64 | Assert.Equal(OrderState.Cancelled, result.Entity!.OrderState); 65 | } 66 | 67 | [Fact] 68 | public async Task WhenConcurrencyExceptionOccurs_ThenShouldReturnConflict() 69 | { 70 | var order = _mapper.Map(Orders.Order1); 71 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 72 | .ReturnsAsync(order); 73 | _repositoryMoq.Setup(x => x.UpdateOrderStateAsync(It.IsAny(), It.IsAny())) 74 | .ThrowsAsync(new ConcurrencyException()); 75 | var handler = new CancelOrderHandler(_repositoryMoq.Object); 76 | 77 | var result = await handler.Handle(new CancelOrder(Guid.Empty), default); 78 | 79 | Assert.NotNull(result); 80 | Assert.Equal(CommandOutcome.Conflict, result.Outcome); 81 | } 82 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/CommandHandlers/CreateOrderHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AutoMapper; 3 | using EventDriven.CQRS.Abstractions.Commands; 4 | using Moq; 5 | using OrderService.Domain.OrderAggregate; 6 | using OrderService.Domain.OrderAggregate.Commands; 7 | using OrderService.Domain.OrderAggregate.CommandHandlers; 8 | using OrderService.Repositories; 9 | using OrderService.Tests.Fakes; 10 | using OrderService.Tests.Helpers; 11 | using Xunit; 12 | 13 | namespace OrderService.Tests.Domain.OrderAggregate.CommandHandlers; 14 | 15 | public class CreateOrderHandlerTests 16 | { 17 | private readonly IMapper _mapper; 18 | 19 | private readonly Mock _repositoryMoq; 20 | 21 | public CreateOrderHandlerTests() 22 | { 23 | _repositoryMoq = new Mock(); 24 | _mapper = MappingHelper.GetMapper(); 25 | } 26 | 27 | [Fact] 28 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 29 | { 30 | var handler = new CreateOrderHandler(_repositoryMoq.Object); 31 | 32 | Assert.NotNull(handler); 33 | Assert.IsType(handler); 34 | } 35 | 36 | [Fact] 37 | public async Task WhenOrderDoesNotSaveCorrectly_ThenShouldReturnInvalidCommand() 38 | { 39 | _repositoryMoq.Setup(x => x.AddAsync(It.IsAny())) 40 | .ReturnsAsync((Order) null!); 41 | var handler = new CreateOrderHandler(_repositoryMoq.Object); 42 | 43 | var result = await handler.Handle(new CreateOrder(_mapper.Map(Orders.Order1)), default); 44 | 45 | Assert.NotNull(result); 46 | Assert.Equal(CommandOutcome.InvalidCommand, result.Outcome); 47 | } 48 | 49 | [Fact] 50 | public async Task WhenOrderIsCreated_ThenShouldReturnEntity() 51 | { 52 | var order = _mapper.Map(Orders.Order1); 53 | _repositoryMoq.Setup(x => x.AddAsync(It.IsAny())) 54 | .ReturnsAsync(order); 55 | var handler = new CreateOrderHandler(_repositoryMoq.Object); 56 | 57 | var result = await handler.Handle(new CreateOrder(order), default); 58 | 59 | Assert.NotNull(result); 60 | Assert.Equal(CommandOutcome.Accepted, result.Outcome); 61 | Assert.Equal(order.Id, result.Entity!.Id); 62 | } 63 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/CommandHandlers/RemoveOrderHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using AutoMapper; 5 | using EventDriven.CQRS.Abstractions.Commands; 6 | using Moq; 7 | using OrderService.Domain.OrderAggregate; 8 | using OrderService.Domain.OrderAggregate.Commands; 9 | using OrderService.Domain.OrderAggregate.CommandHandlers; 10 | using OrderService.Repositories; 11 | using OrderService.Tests.Fakes; 12 | using OrderService.Tests.Helpers; 13 | using Xunit; 14 | 15 | namespace OrderService.Tests.Domain.OrderAggregate.CommandHandlers 16 | { 17 | public class RemoveOrderHandlerTests 18 | { 19 | private readonly Mock _repositoryMoq; 20 | private readonly IMapper _mapper; 21 | 22 | public RemoveOrderHandlerTests() 23 | { 24 | _repositoryMoq = new Mock(); 25 | _mapper = MappingHelper.GetMapper(); 26 | } 27 | 28 | [Fact] 29 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 30 | { 31 | var handler = new RemoveOrderHandler(_repositoryMoq.Object); 32 | 33 | Assert.NotNull(handler); 34 | Assert.IsType(handler); 35 | } 36 | 37 | [Fact] 38 | public async Task WhenRemoved_ThenShouldReturnAccepted() 39 | { 40 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 41 | .ReturnsAsync(_mapper.Map(Orders.Order1)); 42 | var handler = new RemoveOrderHandler(_repositoryMoq.Object); 43 | 44 | var result = await handler.Handle(new RemoveOrder(Guid.Empty), CancellationToken.None); 45 | 46 | Assert.NotNull(result); 47 | Assert.Equal(CommandOutcome.Accepted, result.Outcome); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/CommandHandlers/ShipOrderHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using EventDriven.CQRS.Abstractions.Commands; 5 | using EventDriven.DDD.Abstractions.Repositories; 6 | using Moq; 7 | using OrderService.Domain.OrderAggregate; 8 | using OrderService.Domain.OrderAggregate.Commands; 9 | using OrderService.Domain.OrderAggregate.CommandHandlers; 10 | using OrderService.Repositories; 11 | using OrderService.Tests.Fakes; 12 | using OrderService.Tests.Helpers; 13 | using Xunit; 14 | 15 | namespace OrderService.Tests.Domain.OrderAggregate.CommandHandlers 16 | { 17 | public class ShipOrderHandlerTests 18 | { 19 | private readonly IMapper _mapper; 20 | private readonly Mock _repositoryMoq; 21 | 22 | public ShipOrderHandlerTests() 23 | { 24 | _repositoryMoq = new Mock(); 25 | _mapper = MappingHelper.GetMapper(); 26 | } 27 | 28 | [Fact] 29 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 30 | { 31 | var handler = new ShipOrderHandler(_repositoryMoq.Object); 32 | 33 | Assert.NotNull(handler); 34 | Assert.IsType(handler); 35 | } 36 | 37 | [Fact] 38 | public async Task WhenOrderDoesNotExist_ThenShouldReturnNotFound() 39 | { 40 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 41 | .ReturnsAsync((Order) null!); 42 | var handler = new ShipOrderHandler(_repositoryMoq.Object); 43 | 44 | var result = await handler.Handle(new ShipOrder(Guid.Empty), default); 45 | 46 | Assert.NotNull(result); 47 | Assert.Equal(CommandOutcome.NotFound, result.Outcome); 48 | } 49 | 50 | [Fact] 51 | public async Task WhenOrderFailsToUpdate_ThenShouldReturnNotFound() 52 | { 53 | var order = _mapper.Map(Orders.Order1); 54 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 55 | .ReturnsAsync(order); 56 | _repositoryMoq.Setup(x => x.UpdateOrderStateAsync(It.IsAny(), It.IsAny())) 57 | .ReturnsAsync((Order) null!); 58 | var handler = new ShipOrderHandler(_repositoryMoq.Object); 59 | 60 | var result = await handler.Handle(new ShipOrder(Guid.Empty), default); 61 | 62 | Assert.NotNull(result); 63 | Assert.Equal(CommandOutcome.NotFound, result.Outcome); 64 | } 65 | 66 | [Fact] 67 | public async Task WhenConcurrencyExceptionOccurs_ThenShouldReturnConflict() 68 | { 69 | var order = _mapper.Map(Orders.Order1); 70 | _repositoryMoq.Setup(x => x.GetAsync(It.IsAny())) 71 | .ReturnsAsync(order); 72 | _repositoryMoq.Setup(x => x.UpdateOrderStateAsync(It.IsAny(), It.IsAny())) 73 | .ThrowsAsync(new ConcurrencyException()); 74 | var handler = new ShipOrderHandler(_repositoryMoq.Object); 75 | 76 | var result = await handler.Handle(new ShipOrder(Guid.Empty), default); 77 | 78 | Assert.NotNull(result); 79 | Assert.Equal(CommandOutcome.Conflict, result.Outcome); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/CommandHandlers/UpdateOrderHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AutoMapper; 3 | using EventDriven.CQRS.Abstractions.Commands; 4 | using EventDriven.DDD.Abstractions.Repositories; 5 | using Moq; 6 | using OrderService.Domain.OrderAggregate; 7 | using OrderService.Domain.OrderAggregate.Commands; 8 | using OrderService.Domain.OrderAggregate.CommandHandlers; 9 | using OrderService.Repositories; 10 | using OrderService.Tests.Fakes; 11 | using OrderService.Tests.Helpers; 12 | using Xunit; 13 | 14 | namespace OrderService.Tests.Domain.OrderAggregate.CommandHandlers; 15 | 16 | public class UpdateOrderHandlerTests 17 | { 18 | private readonly IMapper _mapper; 19 | 20 | private readonly Mock _repositoryMoq; 21 | 22 | public UpdateOrderHandlerTests() 23 | { 24 | _repositoryMoq = new Mock(); 25 | _mapper = MappingHelper.GetMapper(); 26 | } 27 | 28 | [Fact] 29 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 30 | { 31 | var handler = new UpdateOrderHandler(_repositoryMoq.Object); 32 | 33 | Assert.NotNull(handler); 34 | Assert.IsType(handler); 35 | } 36 | 37 | [Fact] 38 | public async Task WhenOrderFailsToUpdate_ThenShouldReturnNotFound() 39 | { 40 | _repositoryMoq.Setup(x => x.UpdateAsync(It.IsAny())) 41 | .ReturnsAsync((Order) null!); 42 | var handler = new UpdateOrderHandler(_repositoryMoq.Object); 43 | 44 | var result = await handler.Handle(new UpdateOrder(new Order()), default); 45 | 46 | Assert.NotNull(result); 47 | Assert.Equal(CommandOutcome.NotFound, result.Outcome); 48 | } 49 | 50 | [Fact] 51 | public async Task WhenConcurrencyExceptionOccurs_ThenShouldReturnConflict() 52 | { 53 | _repositoryMoq.Setup(x => x.UpdateAsync(It.IsAny())) 54 | .ThrowsAsync(new ConcurrencyException()); 55 | var handler = new UpdateOrderHandler(_repositoryMoq.Object); 56 | 57 | var result = await handler.Handle(new UpdateOrder(new Order()), default); 58 | 59 | Assert.NotNull(result); 60 | Assert.Equal(CommandOutcome.Conflict, result.Outcome); 61 | } 62 | 63 | [Fact] 64 | public async Task WhenOrderIsUpdated_ThenShouldReturnAcceptedEntity() 65 | { 66 | var order = _mapper.Map(Orders.Order1); 67 | _repositoryMoq.Setup(x => x.UpdateAsync(It.IsAny())) 68 | .ReturnsAsync(order); 69 | var handler = new UpdateOrderHandler(_repositoryMoq.Object); 70 | 71 | var result = await handler.Handle(new UpdateOrder(order), default); 72 | 73 | Assert.NotNull(result); 74 | Assert.Equal(CommandOutcome.Accepted, result.Outcome); 75 | Assert.Equal(order.Id, result.Entity!.Id); 76 | } 77 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/OrderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AutoMapper; 3 | using OrderService.Domain.OrderAggregate; 4 | using OrderService.Domain.OrderAggregate.Commands; 5 | using OrderService.Domain.OrderAggregate.Events; 6 | using OrderService.Tests.Fakes; 7 | using OrderService.Tests.Helpers; 8 | using Xunit; 9 | 10 | namespace OrderService.Tests.Domain.OrderAggregate; 11 | 12 | public class OrderTests 13 | { 14 | private readonly IMapper _mapper; 15 | 16 | public OrderTests() => _mapper = MappingHelper.GetMapper(); 17 | 18 | [Fact] 19 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 20 | { 21 | var order = new Order(); 22 | 23 | Assert.NotNull(order); 24 | Assert.IsType(order); 25 | } 26 | 27 | [Fact] 28 | public void WhenProcessingCreateOrderCommand_ThenShouldReturnOrderCreated() 29 | { 30 | var orderIn = _mapper.Map(Orders.Order1); 31 | var order = new Order(); 32 | 33 | var @event = order.Process(new CreateOrder(orderIn)); 34 | 35 | Assert.NotNull(@event); 36 | Assert.IsAssignableFrom(@event); 37 | } 38 | 39 | [Fact] 40 | public void WhenApplyingOrderCreatedEvent_ThenShouldHaveIdSet() 41 | { 42 | var order = new Order 43 | { 44 | Id = Guid.NewGuid() 45 | }; 46 | var createdEvent = new OrderCreated(order); 47 | 48 | order.Apply(createdEvent); 49 | 50 | Assert.Equal(createdEvent.EntityId, order.Id); 51 | } 52 | 53 | [Fact] 54 | public void WhenProcessingUpdateOrderCommand_ThenShouldReturnOrderUpdated() 55 | { 56 | var orderIn = _mapper.Map(Orders.Order1); 57 | var order = new Order(); 58 | 59 | var @event = order.Process(new UpdateOrder(orderIn)); 60 | 61 | Assert.NotNull(@event); 62 | Assert.IsAssignableFrom(@event); 63 | } 64 | 65 | [Fact] 66 | public void WhenApplyingOrderUpdatedEvent_ThenShouldHaveETagSet() 67 | { 68 | var order = new Order 69 | { 70 | Id = Guid.NewGuid() 71 | }; 72 | var createdEvent = new OrderUpdated(order); 73 | 74 | order.Apply(createdEvent); 75 | 76 | Assert.Equal(createdEvent.EntityETag, order.ETag); 77 | } 78 | 79 | [Fact] 80 | public void WhenProcessingRemoveOrderCommand_ThenShouldReturnOrderRemoved() 81 | { 82 | var order = new Order(); 83 | 84 | var @event = order.Process(new RemoveOrder(Guid.NewGuid())); 85 | 86 | Assert.NotNull(@event); 87 | Assert.IsAssignableFrom(@event); 88 | } 89 | 90 | [Fact] 91 | public void WhenApplyingOrderRemovedEvent_ThenShouldBeNotNull() 92 | { 93 | var order = new Order(); 94 | var orderRemoved = new OrderRemoved(Guid.NewGuid()); 95 | 96 | order.Apply(orderRemoved); 97 | 98 | Assert.NotNull(order); 99 | } 100 | 101 | [Fact] 102 | public void WhenProcessingShipOrderCommand_ThenShouldReturnOrderShipped() 103 | { 104 | var orderIn = _mapper.Map(Orders.Order1); 105 | var order = new Order(); 106 | 107 | var events = order.Process(new ShipOrder(orderIn.Id)); 108 | 109 | Assert.NotNull(events); 110 | Assert.IsAssignableFrom(events); 111 | } 112 | 113 | [Fact] 114 | public void WhenApplyingOrderShippedEvent_ThenShouldBeInShippedState() 115 | { 116 | var orderIn = _mapper.Map(Orders.Order1); 117 | var shippedEvent = new OrderShipped(orderIn.Id); 118 | 119 | orderIn.Apply(shippedEvent); 120 | 121 | Assert.Equal(OrderState.Shipped, orderIn.OrderState); 122 | } 123 | 124 | [Fact] 125 | public void WhenProcessingCancelOrderCommand_ThenShouldReturnOrderCancelled() 126 | { 127 | var orderIn = _mapper.Map(Orders.Order1); 128 | var order = new Order(); 129 | 130 | var events = order.Process(new CancelOrder(orderIn.Id)); 131 | 132 | Assert.NotNull(events); 133 | Assert.IsAssignableFrom(events); 134 | } 135 | 136 | [Fact] 137 | public void WhenApplyingOrderCancelledEvent_ThenShouldBeInCancelledState() 138 | { 139 | var orderIn = _mapper.Map(Orders.Order1); 140 | var cancelledEvent = new OrderCancelled(orderIn.Id); 141 | 142 | orderIn.Apply(cancelledEvent); 143 | 144 | Assert.Equal(OrderState.Cancelled, orderIn.OrderState); 145 | } 146 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/QueryHandlers/GetOrderHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using Moq; 5 | using OrderService.Domain.OrderAggregate; 6 | using OrderService.Domain.OrderAggregate.Queries; 7 | using OrderService.Domain.OrderAggregate.QueryHandlers; 8 | using OrderService.Repositories; 9 | using OrderService.Tests.Fakes; 10 | using OrderService.Tests.Helpers; 11 | using Xunit; 12 | 13 | namespace OrderService.Tests.Domain.OrderAggregate.QueryHandlers; 14 | 15 | public class GetOrderHandlerTests 16 | { 17 | private readonly IMapper _mapper; 18 | private readonly Mock _repositoryMock; 19 | 20 | public GetOrderHandlerTests() 21 | { 22 | _repositoryMock = new Mock(); 23 | _mapper = MappingHelper.GetMapper(); 24 | } 25 | 26 | [Fact] 27 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 28 | { 29 | var handler = new GetOrderHandler(_repositoryMock.Object); 30 | 31 | Assert.NotNull(handler); 32 | Assert.IsType(handler); 33 | } 34 | 35 | [Fact] 36 | public async Task WhenRetrievingEntity_ThenEntityShouldBeReturned() 37 | { 38 | var expected = _mapper.Map(Orders.Order1); 39 | _repositoryMock.Setup(x => x.GetAsync(It.IsAny())) 40 | .ReturnsAsync(expected); 41 | 42 | var handler = new GetOrderHandler(_repositoryMock.Object); 43 | 44 | var actual = await handler.Handle(new GetOrder(Guid.Empty), default); 45 | 46 | Assert.Equal(expected, actual); 47 | } 48 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/QueryHandlers/GetOrdersByCustomerHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using AutoMapper; 5 | using Moq; 6 | using OrderService.Domain.OrderAggregate; 7 | using OrderService.Domain.OrderAggregate.Queries; 8 | using OrderService.Domain.OrderAggregate.QueryHandlers; 9 | using OrderService.Repositories; 10 | using OrderService.Tests.Fakes; 11 | using OrderService.Tests.Helpers; 12 | using Xunit; 13 | 14 | namespace OrderService.Tests.Domain.OrderAggregate.QueryHandlers; 15 | 16 | public class GetOrdersByCustomerHandlerTests 17 | { 18 | private readonly IMapper _mapper; 19 | private readonly Mock _repositoryMock; 20 | 21 | public GetOrdersByCustomerHandlerTests() 22 | { 23 | _repositoryMock = new Mock(); 24 | _mapper = MappingHelper.GetMapper(); 25 | } 26 | 27 | [Fact] 28 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 29 | { 30 | var handler = new GetOrdersByCustomerHandler(_repositoryMock.Object); 31 | 32 | Assert.NotNull(handler); 33 | Assert.IsType(handler); 34 | } 35 | 36 | [Fact] 37 | public async Task WhenRetrievingEntities_ThenAllEntitiesShouldBeReturned() 38 | { 39 | _repositoryMock.Setup(x => x.GetByCustomerAsync(It.IsAny())) 40 | .ReturnsAsync(new List 41 | { 42 | _mapper.Map(Orders.Order1), 43 | _mapper.Map(Orders.Order2) 44 | }); 45 | 46 | var handler = new GetOrdersByCustomerHandler(_repositoryMock.Object); 47 | 48 | var result = await handler.Handle(new GetOrdersByCustomer(Guid.Empty), default); 49 | 50 | Assert.Collection(result, 51 | c => Assert.Equal(OrderViews.Order1.Id, c.Id), 52 | c => Assert.Equal(OrderViews.Order2.Id, c.Id)); 53 | } 54 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Domain/OrderAggregate/QueryHandlers/GetOrdersHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using Moq; 5 | using OrderService.Domain.OrderAggregate; 6 | using OrderService.Domain.OrderAggregate.Queries; 7 | using OrderService.Domain.OrderAggregate.QueryHandlers; 8 | using OrderService.Repositories; 9 | using OrderService.Tests.Fakes; 10 | using OrderService.Tests.Helpers; 11 | using Xunit; 12 | 13 | namespace OrderService.Tests.Domain.OrderAggregate.QueryHandlers; 14 | 15 | public class GetOrdersHandlerTests 16 | { 17 | private readonly IMapper _mapper; 18 | private readonly Mock _repositoryMock; 19 | 20 | public GetOrdersHandlerTests() 21 | { 22 | _repositoryMock = new Mock(); 23 | _mapper = MappingHelper.GetMapper(); 24 | } 25 | 26 | [Fact] 27 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 28 | { 29 | var handler = new GetOrdersHandler(_repositoryMock.Object); 30 | 31 | Assert.NotNull(handler); 32 | Assert.IsType(handler); 33 | } 34 | 35 | [Fact] 36 | public async Task WhenRetrievingEntities_ThenAllEntitiesShouldBeReturned() 37 | { 38 | _repositoryMock.Setup(x => x.GetAsync()) 39 | .ReturnsAsync(new List 40 | { 41 | _mapper.Map(Orders.Order1), 42 | _mapper.Map(Orders.Order2) 43 | }); 44 | 45 | var handler = new GetOrdersHandler(_repositoryMock.Object); 46 | 47 | var result = await handler.Handle(new GetOrders(), default); 48 | 49 | Assert.Collection(result, 50 | c => Assert.Equal(OrderViews.Order1.Id, c.Id), 51 | c => Assert.Equal(OrderViews.Order2.Id, c.Id)); 52 | } 53 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Fakes/Orders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using OrderService.DTO.Read; 4 | using OrderService.DTO.Write; 5 | using OrderState = OrderService.DTO.Read.OrderState; 6 | 7 | namespace OrderService.Tests.Fakes; 8 | 9 | using ReadOrderState = OrderState; 10 | using WriteOrderState = DTO.Write.OrderState; 11 | 12 | public static class Orders 13 | { 14 | public static Order Order1 => new() 15 | { 16 | Id = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 17 | CustomerId = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 18 | OrderDate = DateTime.Now, 19 | OrderItems = new List 20 | { 21 | new(Guid.Parse("3fa85f64-5717-4562-b3fc-2c963f66afa6"), "Espresso", 2.5M), 22 | new(Guid.Parse("2b3adade-9499-4673-8e0a-8e4b5e91ddb1"), "Cappuccino", 3.5M), 23 | new(Guid.Parse("2d14cb8b-7400-4843-b4be-6b6ec68ee654"), "Latte", 4.5M) 24 | }, 25 | ShippingAddress = new Address 26 | { 27 | Street = "123 This Street", 28 | City = "Freemont", 29 | State = "CA", 30 | Country = "USA", 31 | PostalCode = "90045" 32 | }, 33 | OrderState = WriteOrderState.Created 34 | }; 35 | 36 | public static Order Order2 => new() 37 | { 38 | Id = Guid.Parse("39949530-cc63-4aaa-bc65-49cdfd888b17"), 39 | CustomerId = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 40 | OrderDate = DateTime.Now, 41 | OrderItems = new List 42 | { 43 | new(Guid.Parse("6875b6dd-2e00-460d-aa93-695852dcc1e8"), "Coke", 2.5M), 44 | new(Guid.Parse("cda037c0-688a-408c-9eb4-33c9b580d260"), "Squirt", 3.5M), 45 | new(Guid.Parse("b26462b9-0efa-49e1-8639-9a80ed47829c"), "Fresca", 4.5M) 46 | }, 47 | ShippingAddress = new Address 48 | { 49 | Street = "123 That Street", 50 | City = "Dallas", 51 | State = "TX", 52 | Country = "USA", 53 | PostalCode = "70023" 54 | }, 55 | OrderState = WriteOrderState.Created 56 | }; 57 | } 58 | 59 | public static class OrderViews 60 | { 61 | public static OrderView Order1 => new() 62 | { 63 | Id = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 64 | CustomerId = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 65 | OrderDate = DateTime.Now, 66 | OrderTotal = 212, 67 | Street = "123 This Street", 68 | City = "Freemont", 69 | State = "CA", 70 | Country = "USA", 71 | PostalCode = "90045", 72 | OrderState = ReadOrderState.Created 73 | }; 74 | 75 | public static OrderView Order2 => new() 76 | { 77 | Id = Guid.Parse("39949530-cc63-4aaa-bc65-49cdfd888b17"), 78 | CustomerId = Guid.Parse("22eea083-6f0d-48f2-8c82-65ac850e5aad"), 79 | OrderDate = DateTime.Now, 80 | OrderTotal = 212, 81 | Street = "123 That Street", 82 | City = "Dallas", 83 | State = "TX", 84 | Country = "USA", 85 | PostalCode = "70023", 86 | OrderState = ReadOrderState.Created 87 | }; 88 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Helpers/MappingHelper.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using OrderService.Mapping; 3 | 4 | namespace OrderService.Tests.Helpers; 5 | 6 | public static class MappingHelper 7 | { 8 | public static IMapper GetMapper() 9 | { 10 | var config = new MapperConfiguration( 11 | cfg => { cfg.AddProfile(typeof(AutoMapperProfile)); }); 12 | 13 | return config.CreateMapper(); 14 | } 15 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/Integration/EventHandlers/CustomerAddressUpdatedEventHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AutoFixture; 4 | using AutoMapper; 5 | using Common.Integration.Events; 6 | using EventDriven.EventBus.Abstractions; 7 | using Microsoft.Extensions.Logging; 8 | using Moq; 9 | using OrderService.Domain.OrderAggregate; 10 | using OrderService.Integration.EventHandlers; 11 | using OrderService.Repositories; 12 | using OrderService.Tests.Helpers; 13 | using Xunit; 14 | using Address = Common.Integration.Models.Address; 15 | 16 | namespace OrderService.Tests.Integration.EventHandlers; 17 | 18 | public class CustomerAddressUpdatedEventHandlerTests 19 | { 20 | private readonly Fixture _fixture = new(); 21 | private readonly Mock> _logger; 22 | private readonly IMapper _mapper; 23 | 24 | private readonly Mock _repositoryMoq; 25 | 26 | public CustomerAddressUpdatedEventHandlerTests() 27 | { 28 | _repositoryMoq = new Mock(); 29 | _mapper = MappingHelper.GetMapper(); 30 | _logger = new Mock>(); 31 | } 32 | 33 | [Fact] 34 | public void WhenInstantiated_ThenShouldBeOfCorrectType() 35 | { 36 | var handler = new CustomerAddressUpdatedEventHandler(_repositoryMoq.Object, _mapper, _logger.Object); 37 | 38 | Assert.NotNull(handler); 39 | Assert.IsAssignableFrom>(handler); 40 | Assert.IsType(handler); 41 | } 42 | 43 | [Fact] 44 | public async Task WhenEventIsHandled_ThenOrderAddressShouldGetUpdated() 45 | { 46 | var address = _fixture.Create
(); 47 | var updatedEvent = new CustomerAddressUpdated(Guid.NewGuid(), address); 48 | var addressWasUpdated = false; 49 | _repositoryMoq.Setup(x => x.GetByCustomerAsync(It.IsAny())) 50 | .ReturnsAsync(new[] { _fixture.Create() }); 51 | _repositoryMoq.Setup(x => x.UpdateAddressAsync(It.IsAny(), It.IsAny())) 52 | .Callback((_, _) => { addressWasUpdated = true; }); 53 | var handler = new CustomerAddressUpdatedEventHandler(_repositoryMoq.Object, _mapper, _logger.Object); 54 | 55 | await handler.HandleAsync(updatedEvent); 56 | 57 | Assert.True(addressWasUpdated); 58 | } 59 | } -------------------------------------------------------------------------------- /test/OrderService.Tests/OrderService.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------