├── .github └── workflows │ ├── codeql-analysis.yml │ └── dotnet.yml ├── .gitignore ├── DependenciesGraph.png ├── LICENSE ├── README.md ├── Samples ├── Twilight.Samples.CQRS │ ├── AutofacModule.cs │ ├── DiagnosticsConfig.cs │ ├── Program.cs │ ├── Runner.cs │ └── Twilight.Samples.CQRS.csproj └── Twilight.Samples.Common │ ├── AppHost.cs │ ├── Data │ ├── Entities │ │ ├── UserEntity.cs │ │ └── UserViewEntity.cs │ ├── SampleDataContext.cs │ └── ViewDataContext.cs │ ├── Features │ ├── GetUserById │ │ ├── GetUserByIdCqrsQueryHandler.cs │ │ ├── GetUserByIdQueryParameters.cs │ │ ├── GetUserByIdQueryResponsePayload.cs │ │ └── GetUserByIdQueryValidator.cs │ ├── GetUsersView │ │ ├── GetUsersViewCqrsQueryHandler.cs │ │ ├── GetUsersViewQueryParameters.cs │ │ ├── GetUsersViewQueryResponsePayload.cs │ │ └── GetUsersViewQueryValidator.cs │ └── RegisterUser │ │ ├── NotifyUserRegisteredCqrsEventHandler.cs │ │ ├── RegisterUserCommandParameters.cs │ │ ├── RegisterUserCommandValidator.cs │ │ ├── RegisterUserCqrsCommandHandler.cs │ │ ├── UserRegisteredCqrsEventHandler.cs │ │ ├── UserRegisteredEventParameters.cs │ │ └── UserRegisteredEventValidator.cs │ ├── IAssemblyMarker.cs │ ├── IRunner.cs │ ├── Twilight.Samples.Common.csproj │ └── Views │ └── UserView.cs ├── Src ├── Twilight.CQRS.Autofac │ ├── ContainerBuilderExtensions.cs │ ├── Twilight.CQRS.Autofac.csproj │ └── Twilight.CQRS.Autofac.xml ├── Twilight.CQRS.Interfaces │ ├── ICqrsCommand.cs │ ├── ICqrsCommandHandler.cs │ ├── ICqrsEvent.cs │ ├── ICqrsEventHandler.cs │ ├── ICqrsMessage.cs │ ├── ICqrsMessageHandler.cs │ ├── ICqrsQuery.cs │ ├── ICqrsQueryHandler.cs │ ├── Twilight.CQRS.Interfaces.csproj │ └── Twilight.CQRS.Interfaces.xml ├── Twilight.CQRS.Messaging.InMemory.Autofac │ ├── AutofacInMemoryMessageSender.cs │ ├── AutofacInMemoryMessagingRegistrationExtensions.cs │ ├── Twilight.CQRS.Messaging.InMemory.Autofac.csproj │ └── Twilight.CQRS.Messaging.InMemory.Autofac.xml ├── Twilight.CQRS.Messaging.Interfaces │ ├── IMessageSender.cs │ ├── Twilight.CQRS.Messaging.Interfaces.csproj │ └── Twilight.CQRS.Messaging.Interfaces.xml └── Twilight.CQRS │ ├── Commands │ ├── CqrsCommand.cs │ ├── CqrsCommandHandlerBase.cs │ └── CqrsCommandResponse.cs │ ├── CqrsMessage.cs │ ├── CqrsMessageHandler.cs │ ├── Events │ ├── CqrsEvent.cs │ └── CqrsEventHandlerBase.cs │ ├── Queries │ ├── CqrsQuery.cs │ ├── CqrsQueryHandlerBase.cs │ └── QueryResponse.cs │ ├── Twilight.CQRS.csproj │ └── Twilight.CQRS.xml ├── Test ├── Twilight.CQRS.Autofac.Tests.Unit │ ├── CqrsRegistrationExtensionsTests.cs │ ├── Setup │ │ └── TestCqrsCommandHandler.cs │ └── Twilight.CQRS.Autofac.Tests.Unit.csproj ├── Twilight.CQRS.Benchmarks │ ├── AutofacModule.cs │ ├── Commands │ │ ├── SendCommandReceived.cs │ │ ├── SendCommandValidator.cs │ │ ├── SendCqrsCommand.cs │ │ └── SendCqrsCommandHandler.cs │ ├── Events │ │ ├── SendCqrsEvent.cs │ │ ├── SendCqrsEventHandler.cs │ │ └── SendEventValidator.cs │ ├── InMemoryBenchmarks.cs │ ├── MessageParameters.cs │ ├── Program.cs │ ├── Queries │ │ ├── QueryResponsePayload.cs │ │ ├── SendCqrsQuery.cs │ │ ├── SendCqrsQueryHandler.cs │ │ └── SendQueryValidator.cs │ └── Twilight.CQRS.Benchmarks.csproj ├── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration │ ├── AutofacDependencyResolutionFailureTests.cs │ ├── AutofacInMemoryMessageSenderFailureTests.cs │ ├── AutofacInMemoryMessageSenderTests.cs │ ├── Setup │ │ ├── Handlers │ │ │ ├── MultipleHandlersOneHandler.cs │ │ │ ├── MultipleHandlersTwoHandler.cs │ │ │ ├── TestCqrsCommandHandler.cs │ │ │ ├── TestCqrsCommandWithResponseHandler.cs │ │ │ ├── TestCqrsEventHandler.cs │ │ │ └── TestCqrsQueryHandler.cs │ │ ├── ITestService.cs │ │ ├── IVerifier.cs │ │ ├── IntegrationTestBase.cs │ │ ├── IntegrationTestModule.cs │ │ ├── Parameters │ │ │ └── MultipleHandlersParameters.cs │ │ ├── TestService.cs │ │ └── Validators │ │ │ ├── MultipleHandlersValidator.cs │ │ │ ├── TestCommandValidator.cs │ │ │ ├── TestCommandWithResponseValidator.cs │ │ │ ├── TestEventValidator.cs │ │ │ └── TestQueryValidator.cs │ └── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.csproj ├── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit │ ├── AutofacInMemoryMessagingRegistrationExtensionsTests.cs │ └── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit.csproj ├── Twilight.CQRS.Tests.Common │ ├── Constants.cs │ ├── NonValidatingTestParameters.cs │ ├── TestParameters.cs │ └── Twilight.CQRS.Tests.Common.csproj └── Twilight.CQRS.Tests.Unit │ ├── Commands │ ├── CommandHandlerTests.cs │ ├── CommandTests.cs │ └── TestCqrsCommandHandler.cs │ ├── Events │ └── EventTests.cs │ ├── Queries │ ├── NonValidatingTestCqrsQueryHandler.cs │ ├── QueryHandlerTests.cs │ ├── QueryTests.cs │ ├── TestCqrsQueryHandler.cs │ ├── TestQueryParametersValidator.cs │ └── TestQueryResponse.cs │ ├── TestParametersValidator.cs │ └── Twilight.CQRS.Tests.Unit.csproj ├── Twilight.sln ├── Twilight.sln.DotSettings ├── embold.yaml └── upgrade-assistant.clef /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '0 3 * * *' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'csharp' ] 24 | 25 | steps: 26 | 27 | - name: Checkout Repository 28 | uses: actions/checkout@v2 29 | 30 | - name: Setup .NET 31 | uses: actions/setup-dotnet@v3 32 | with: 33 | dotnet-version: 9.x.x 34 | 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v2 37 | with: 38 | languages: ${{ matrix.language }} 39 | 40 | - name: Restore Dependencies 41 | run: dotnet restore 42 | 43 | - name: Build 44 | run: dotnet build --no-restore 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v2 48 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | dotnet-version: ['9.x.x' ] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: ${{ matrix.dotnet-version }} 23 | - name: Display dotnet version 24 | run: dotnet --version 25 | - name: Restore Dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --configuration Release --no-restore --output buildOutput 29 | - name: Run Tests 30 | run: dotnet test --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}" --no-restore 31 | - name: Upload Test Results 32 | uses: actions/upload-artifact@v2 33 | 34 | with: 35 | name: dotnet-results-${{ matrix.dotnet-version }} 36 | path: TestResults-${{ matrix.dotnet-version }} 37 | if: ${{ always() }} 38 | 39 | - name: Generate SBOM 40 | run: | 41 | curl -Lo $RUNNER_TEMP/sbom-tool https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64 42 | chmod +x $RUNNER_TEMP/sbom-tool 43 | $RUNNER_TEMP/sbom-tool generate -b ./buildOutput -bc . -pn Twilight -pv 1.0.0 -ps VerifiedCoder -nsb https://github.com/verifiedcoder/Twilight -V Verbose 44 | 45 | - name: Upload Build Artifact 46 | uses: actions/upload-artifact@v3.1.0 47 | with: 48 | path: buildOutput 49 | -------------------------------------------------------------------------------- /.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 | *.sonarqube/ 13 | RunSonarQube.bat 14 | Twilight.sln.DotSettings 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | #GhostDoc Dictionaries 368 | *.dic -------------------------------------------------------------------------------- /DependenciesGraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verifiedcoder/Twilight/058cfec15b31e0df322134d93412d0e6c918eadd/DependenciesGraph.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Richard Weeks 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilight 2 | 3 | See the [LICENSE](LICENSE) file for legal guidance on using the code in this repository. The MIT lcense was dropped as that could expose the author to potential software patent abuse by external consumers of this code. 4 | 5 | ## Introduction 6 | 7 | Twilight is an implementation of the CQRS design pattern that, 'out-of-the-box', provides an in-memory message transport that uses [Autofac](https://autofac.org/) to register its components. It is meant to be used as a learning resource for the basis of a full-featured system. 8 | 9 | It is not intended for this repository to be published as NuGet packages as support is not possible. The expectation is to use this repository as an educational resource and fork for your own specific requirements. It will be periodically updated to the latest Long Term Support (LTS) version of .Net. 10 | 11 | ### Requirements 12 | 13 | - Microsoft Visual Studio 2022 14 | - .NET 7.0 15 | 16 | ## CQRS 17 | 18 | CQRS stands for Command Query Responsibility Segregation, i.e. keep actions that change data separate from actions that query data sources. 19 | 20 | In its essence, CQRS enables the separation of a read model from a write model. You can use different data sources for your read and write models - or not. It also makes it extremely easy to keep separate concerns just that - separate. 21 | 22 | CQRS is often confused with Event Sourcing (ES) as CQRS can form a solid foundation to support ES (e.g. querying in ES is difficult without CQRS or some similar mechanism) but ES is not required for CQRS. 23 | 24 | This repository currently provides an example of CQRS but remember - you don't always need to use CQRS. When implementing a solution, don't over-engineer and always ask yourself if the implemented architecture can be simplified. 25 | 26 | The sample in this repository is trivial in order to clearly demonstrate usage. 27 | 28 | Further information on the use-case for CQRS: [https://microservices.io/patterns/data/cqrs.html](https://microservices.io/patterns/data/cqrs.html) 29 | 30 | ## Event Sourcing (ES) 31 | 32 | As we have touched on the topic above, a short expansion on what ES provides is warranted. 33 | 34 | ES persists the state of a business entity as a sequence of state-changing events. There is no static 'CRUD' data model. 35 | 36 | The current state of a domain entity is 'rebuilt' by replaying recorded events until the current state is reached. A bank account is a good example - there is no recorded account balance, rather the balance is calculated by replaying the deposit, transfer and withdrawal messages for that account until a current account balance is reached. 37 | 38 | ES is a non-trivial pattern that requires a deep understanding of many advanced topics and patterns to implement and therefore is not suitable for all business use-cases. It can lead to an overly complicated architecture and an excessive maintenance overhead. 39 | 40 | Further information on the use-case for ES: [https://microservices.io/patterns/data/event-sourcing.html](https://microservices.io/patterns/data/event-sourcing.html) 41 | 42 | ## CQRS Components 43 | 44 | CQRS revolves around the production and consumption of messages, of which there are three types: commands, queries and events. 45 | 46 | ### Messages 47 | 48 | A message conveys intent and purpose within a software system. Messages have a unique identifier (the message id) and a correlation id that identifies a series of actions in a workflow. In addition, there is a causation identifier. The causation identifier specifies which message (if any) caused the current message to be produced. The correlation id, together with the causation id enables the tracing of the entire journey a message may take through a system. 49 | 50 | Messages form the basis of CQRS commands, queries and events. A command, query or event is a message: something you send to a recipient. 51 | 52 | #### Commands 53 | 54 | Commands are messages that effect change in a system (like updating a record in a database). A command can have zero or more parameters. 55 | 56 | Strictly, commands should be fire-and-forget. This can be a difficult concept to take on board as the retrieval of the result of a command (e.g. the identifier for a created entity) is decoupled from the process that creates it. 57 | 58 | #### Queries 59 | 60 | Queries request information from a system and receive a response payload containing the requested data (if found). A query may or may not have a request payload that is used to shape the data in the response payload. 61 | 62 | #### Events 63 | 64 | Events are often published in order to tell other parts of a system that a change has occurred, for example to update a data view for reporting analysis. An event may or may not contain a payload with information useful to a party listening for the event. 65 | 66 | ### Handlers 67 | 68 | A handler is required to act as a middleman between messages and the intended destination(s) for their payloads. A handler will consume a message, decide what to do with it (e.g. call a downstream service) and may return a response (that can be used to indicate success or contain a payload). Any error encountered while handling a message (such as the message failing validation) causes an exception to be thrown. 69 | 70 | There can only be one handler for a specific command or query. Unlike commands and queries, events can be consumed by multiple handlers (a powerful capability of CQRS). 71 | 72 | ## Use of Result Pattern 73 | The Results pattern is a design pattern used to encapsulate the outcome of an operation, whether it succeeds or fails, along with additional contextual information. It promotes cleaner and more predictable code by separating the handling of successful and failed outcomes. 74 | 75 | Twilight uses [FluentResults](https://github.com/altmann/FluentResults), a popular NuGet package that implements the Results pattern in C#. It provides a rich set of classes and methods for working with results and is widely used in C# applications for robust error handling and outcome management. 76 | 77 | This pattern fosters cleaner, more maintainable code by separating the logic for handling success and failure outcomes, improving code readability, and facilitating better error reporting and debugging. 78 | 79 | ## Architecture 80 | 81 | Twilight CQRS is broken into discrete areas of functionality in order to best separate concerns. It allows the implementer to use as much or as little of the code as possible without attaching unwanted dependencies. 82 | 83 | The following dependency graph shows the careful planning of the relationships between the components of Twilight CQRS. 84 | 85 | ![Dependencies Graph](DependenciesGraph.png) 86 | 87 | ## API Documentation 88 | 89 | The XML documentation for the public API can be used to generate full developer documentation for Twilight. 90 | 91 | You can use DocFX to generate the documentation. For more information, see [here](https://dotnet.github.io/docfx/). If you run into issues, try pointing your docfx.json to your build assemblies, instead of the project files or source code. 92 | 93 | ## The Sample 94 | 95 | The sample exists to show the mechanics of creating, sending and handling commands, queries and events. 96 | 97 | As an implementer, much of the structure of messages and handlers is up to you. Strive for as much simplicity as possible. All too often, applications are routinely over-engineered. 98 | 99 | The samples are stripped down to make them easy to follow and makes use of Open Telemetry to illustrate the path of a message through the system. Using activity identifiers, correlation and causation, you can easily build a full path and timeline of all system interactions. 100 | 101 | ## Naming Twilight 102 | 103 | This project started out some time ago with a grander aim and was going to use the Paramore Brighter and Darker repositories. When that was dropped as being unnecessarily ambitious, the name as a combination of the Brighter and Darker, Twilight, stuck with attendant *"can you see what I did there?"* gusto. 104 | 105 | The Brighter and Darker projects are excellent examples of how complicated the topic of CQRS can get! 106 | 107 | Paramore, incidentally, is also the name of an American rock group and *"Brighter"* is the fourth track from their first album, *"All We Know is Falling"*. Paramore also have a track called, *"Decode"* and that was the second song played in the end credits of the 2008 romantic fantasy film, *"Twilight"*. There you go. 108 | 109 | ## Sources 110 | 111 | We all learn from the teachings and example of others and this project is no exception. The following sources provided pointers and inspiration for this repository: 112 | 113 | - [https://github.com/jbogard/MediatR](https://github.com/jbogard/MediatR) 114 | - [https://martinfowler.com/bliki/CQRS.html](https://martinfowler.com/bliki/CQRS.html) 115 | - [https://martinfowler.com/eaaDev/EventSourcing.html](https://martinfowler.com/eaaDev/EventSourcing.html) 116 | 117 | ## Note 118 | 119 | Twilight incorporates [Open Telemetry](https://opentelemetry.io/). A console exporter has been added to the sample so you can see the data. 120 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/AutofacModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Twilight.CQRS.Autofac; 3 | using Twilight.CQRS.Messaging.InMemory.Autofac; 4 | using Twilight.Samples.Common; 5 | 6 | namespace Twilight.Samples.CQRS; 7 | 8 | internal sealed class AutofacModule : Module 9 | { 10 | protected override void Load(ContainerBuilder builder) 11 | => builder.RegisterCqrs(typeof(IAssemblyMarker).Assembly) 12 | .AddAutofacInMemoryMessaging() 13 | .RegisterAssemblyTypes(ThisAssembly, new[] { "Runner" }); 14 | } 15 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/DiagnosticsConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Twilight.Samples.CQRS; 4 | 5 | public static class DiagnosticsConfig 6 | { 7 | public const string ServiceName = "Twilight.Samples.CQRS"; 8 | 9 | public static readonly ActivitySource ActivitySource = new(ServiceName); 10 | } -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/Program.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Autofac.Extensions.DependencyInjection; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using OpenTelemetry.Metrics; 8 | using OpenTelemetry.Resources; 9 | using OpenTelemetry.Trace; 10 | using Serilog; 11 | using Twilight.Samples.Common; 12 | using Twilight.Samples.Common.Data; 13 | using Twilight.Samples.CQRS; 14 | 15 | const string consoleOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"; 16 | 17 | Log.Logger = new LoggerConfiguration().MinimumLevel.Verbose() 18 | .Enrich.WithProperty("ApplicationName", DiagnosticsConfig.ServiceName) 19 | .WriteTo.Console(outputTemplate: consoleOutputTemplate) 20 | .CreateBootstrapLogger(); 21 | 22 | var builder = WebApplication.CreateBuilder(args); 23 | 24 | builder.Services.AddOpenTelemetry() 25 | .WithTracing(tracerProviderBuilder 26 | => tracerProviderBuilder.AddSource(DiagnosticsConfig.ActivitySource.Name) 27 | .ConfigureResource(resource => resource.AddService(DiagnosticsConfig.ServiceName)) 28 | .AddAspNetCoreInstrumentation() 29 | .AddConsoleExporter()) 30 | .WithMetrics(metricsProviderBuilder 31 | => metricsProviderBuilder.ConfigureResource(resource => resource.AddService(DiagnosticsConfig.ServiceName)) 32 | .AddAspNetCoreInstrumentation() 33 | .AddConsoleExporter()); 34 | 35 | builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()) 36 | .ConfigureContainer(containerBuilder => { containerBuilder.RegisterModule(); }) 37 | .UseSerilog((context, services, configuration) 38 | => configuration.ReadFrom.Configuration(context.Configuration) 39 | .ReadFrom.Services(services) 40 | .MinimumLevel.Verbose() 41 | .Enrich.WithProperty("ApplicationName", DiagnosticsConfig.ServiceName) 42 | .WriteTo.Console(outputTemplate: consoleOutputTemplate)) 43 | .ConfigureServices(services => 44 | { 45 | services.AddDbContext(dbContextOptions => 46 | { 47 | dbContextOptions.UseInMemoryDatabase("DataDb"); 48 | dbContextOptions.EnableSensitiveDataLogging(); 49 | }); 50 | services.AddDbContext(dbContextOptions => 51 | { 52 | dbContextOptions.UseInMemoryDatabase("ViewDb"); 53 | dbContextOptions.EnableSensitiveDataLogging(); 54 | }); 55 | services.AddHostedService(); 56 | }); 57 | 58 | try 59 | { 60 | Log.Information("Starting {AppName}", DiagnosticsConfig.ServiceName); 61 | 62 | await builder.Build().RunAsync(); 63 | 64 | Log.Information("Running {AppName}", DiagnosticsConfig.ServiceName); 65 | } 66 | catch (Exception ex) 67 | { 68 | Log.Fatal(ex, "{AppName} terminated unexpectedly. Message: {ExceptionMessage}", DiagnosticsConfig.ServiceName, ex.Message); 69 | 70 | Environment.Exit(-1); 71 | } 72 | finally 73 | { 74 | Log.Information("Stopping {AppName}", DiagnosticsConfig.ServiceName); 75 | Log.CloseAndFlush(); 76 | 77 | Environment.Exit(0); 78 | } -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/Runner.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.Diagnostics; 3 | using Taikandi; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | using Twilight.CQRS.Queries; 7 | using Twilight.Samples.Common; 8 | using Twilight.Samples.Common.Features.GetUserById; 9 | using Twilight.Samples.Common.Features.GetUsersView; 10 | using Twilight.Samples.Common.Features.RegisterUser; 11 | 12 | namespace Twilight.Samples.CQRS; 13 | 14 | internal sealed class Runner(IMessageSender messageSender) : IRunner 15 | { 16 | public async Task Run() 17 | { 18 | await RegisterUser(); 19 | await GetRegisteredUser(); 20 | await GetUsersView(); 21 | await GetInvalidUsersView(); 22 | } 23 | 24 | private async Task RegisterUser() 25 | { 26 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(RegisterUser)}"); 27 | { 28 | var id = activity?.Id ?? SequentialGuid.NewGuid().ToString(); 29 | var parameters = new RegisterUserCommandParameters("Bilbo", "Baggins"); 30 | var command = new CqrsCommand(parameters, id); 31 | 32 | await messageSender.Send(command); 33 | } 34 | } 35 | 36 | private async Task GetRegisteredUser() 37 | { 38 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(GetRegisteredUser)}"); 39 | { 40 | var id = activity?.Id ?? SequentialGuid.NewGuid().ToString(); 41 | var parameters = new GetUserByIdQueryParameters(1); 42 | var query = new CqrsQuery>(parameters, id); 43 | 44 | var response = await messageSender.Send(query); 45 | 46 | Log.Information("Query response: {@GetRegisteredUserResponse}", response); 47 | } 48 | } 49 | 50 | private async Task GetUsersView() 51 | { 52 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(GetUsersView)}"); 53 | { 54 | var id = activity?.Id ?? SequentialGuid.NewGuid().ToString(); 55 | var parameters = new GetUsersViewQueryParameters(DateTimeOffset.UtcNow.AddDays(-1)); 56 | var query = new CqrsQuery>(parameters, id); 57 | 58 | var response = await messageSender.Send(query); 59 | 60 | Log.Information("Query response: {@GetUsersViewResponse}", response); 61 | } 62 | } 63 | 64 | private async Task GetInvalidUsersView() 65 | { 66 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(GetInvalidUsersView)}"); 67 | { 68 | var id = activity?.Id ?? SequentialGuid.NewGuid().ToString(); 69 | var parameters = new GetUsersViewQueryParameters(DateTimeOffset.UtcNow.AddDays(+1)); 70 | var query = new CqrsQuery>(parameters, id); 71 | 72 | var response = await messageSender.Send(query); 73 | 74 | if (response.IsFailed) 75 | { 76 | Log.Error("Query validation failed:"); 77 | 78 | foreach (var error in response.Errors) 79 | { 80 | Log.Error(" - {ValidationError}", error); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/Twilight.Samples.CQRS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | 12.0 7 | enable 8 | true 9 | enable 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/AppHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Twilight.Samples.Common; 5 | 6 | public sealed class AppHost(IRunner runner, ILogger logger) : IHostedService 7 | { 8 | public async Task StartAsync(CancellationToken cancellationToken) 9 | { 10 | logger.LogInformation("Started {AppHost}.", nameof(AppHost)); 11 | 12 | await runner.Run(); 13 | } 14 | 15 | public async Task StopAsync(CancellationToken cancellationToken) 16 | { 17 | logger.LogInformation("Stopped {AppHost}.", nameof(AppHost)); 18 | 19 | await Task.CompletedTask; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Data/Entities/UserEntity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Twilight.Samples.Common.Data.Entities; 4 | 5 | public class UserEntity 6 | { 7 | public int Id { get; set; } 8 | 9 | [Required] 10 | public string Forename { get; set; } = null!; 11 | 12 | [Required] 13 | public string Surname { get; set; } = null!; 14 | } 15 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Data/Entities/UserViewEntity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Twilight.Samples.Common.Data.Entities; 4 | 5 | public class UserViewEntity 6 | { 7 | public int Id { get; set; } 8 | 9 | [Required] 10 | public int UserId { get; set; } 11 | 12 | [Required] 13 | public string Forename { get; set; } = null!; 14 | 15 | [Required] 16 | public string Surname { get; set; } = null!; 17 | 18 | [Required] 19 | public string FullName { get; set; } = null!; 20 | 21 | [Required] 22 | public DateTimeOffset? RegistrationDate { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Data/SampleDataContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Twilight.Samples.Common.Data.Entities; 3 | 4 | namespace Twilight.Samples.Common.Data; 5 | 6 | public sealed class SampleDataContext(DbContextOptions options) : DbContext(options) 7 | { 8 | public DbSet Users { get; init; } = null!; 9 | } 10 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Data/ViewDataContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Twilight.Samples.Common.Data.Entities; 3 | 4 | namespace Twilight.Samples.Common.Data; 5 | 6 | public sealed class ViewDataContext(DbContextOptions options) : DbContext(options) 7 | { 8 | public DbSet UsersView { get; init; } = null!; 9 | } 10 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUserById/GetUserByIdCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using System.Diagnostics; 3 | using FluentValidation; 4 | using Microsoft.Extensions.Logging; 5 | using Twilight.CQRS.Queries; 6 | using Twilight.Samples.Common.Data; 7 | using Twilight.Samples.Common.Data.Entities; 8 | 9 | namespace Twilight.Samples.Common.Features.GetUserById; 10 | 11 | public sealed class GetUserByIdCqrsQueryHandler(ViewDataContext dataContext, 12 | ILogger logger, 13 | IValidator>> validator) 14 | : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 15 | { 16 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 17 | { 18 | UserViewEntity? userView; 19 | 20 | using (var activity = Activity.Current?.Source.StartActivity("Getting user from view", ActivityKind.Server)) 21 | { 22 | activity?.AddEvent(new ActivityEvent("Get User by ID")); 23 | activity?.SetTag(nameof(GetUserByIdQueryParameters.UserId), query.Params.UserId); 24 | 25 | userView = await dataContext.UsersView.FindAsync([query.Params.UserId], cancellationToken); 26 | } 27 | 28 | if (userView == null) 29 | { 30 | return Result.Fail($"User with Id '{query.Params.UserId}' not found."); 31 | } 32 | 33 | var payload = new GetUserByIdQueryResponsePayload(userView.Id, userView.Forename, userView.Surname); 34 | var response = new QueryResponse(payload, query.CorrelationId, null, query.MessageId); 35 | 36 | Logger.LogInformation("Handled CQRS Query, {QueryTypeName}.", query.GetType().FullName); 37 | 38 | return await Task.FromResult(Result.Ok(response)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUserById/GetUserByIdQueryParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common.Features.GetUserById; 2 | 3 | public sealed record GetUserByIdQueryParameters(int UserId); 4 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUserById/GetUserByIdQueryResponsePayload.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common.Features.GetUserById; 2 | 3 | public sealed record GetUserByIdQueryResponsePayload(int UserId, string Forename, string Surname); 4 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUserById/GetUserByIdQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Queries; 3 | 4 | namespace Twilight.Samples.Common.Features.GetUserById; 5 | 6 | public sealed class GetUserByIdQueryValidator : AbstractValidator>> 7 | { 8 | public GetUserByIdQueryValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.Params.UserId).GreaterThan(0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUsersView/GetUsersViewCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using System.Diagnostics; 3 | using FluentValidation; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Logging; 6 | using Twilight.CQRS.Queries; 7 | using Twilight.Samples.Common.Data; 8 | using Twilight.Samples.Common.Data.Entities; 9 | using Twilight.Samples.Common.Views; 10 | 11 | namespace Twilight.Samples.Common.Features.GetUsersView; 12 | 13 | public sealed class GetUsersViewCqrsQueryHandler(ViewDataContext dataContext, 14 | ILogger logger, 15 | IValidator>> validator) 16 | : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 17 | { 18 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 19 | { 20 | List? userViews; 21 | 22 | using (var activity = Activity.Current?.Source.StartActivity("Get all users in view", ActivityKind.Server)) 23 | { 24 | activity?.AddEvent(new ActivityEvent("Get All Users")); 25 | 26 | userViews = await dataContext.UsersView.Where(u => u.RegistrationDate >= query.Params.RegistrationDate) 27 | .OrderBy(v => v.RegistrationDate) 28 | .ToListAsync(cancellationToken); 29 | } 30 | 31 | var usersView = userViews.Select(u => new UserView(u.Id, u.UserId, u.Forename, u.Surname, u.FullName, u.RegistrationDate)); 32 | 33 | var payload = new GetUsersViewQueryResponsePayload(usersView); 34 | var response = new QueryResponse(payload, query.CorrelationId, null, query.MessageId); 35 | 36 | Logger.LogInformation("Handled CQRS Query, {QueryTypeName}.", query.GetType().FullName); 37 | 38 | return await Task.FromResult(Result.Ok(response)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUsersView/GetUsersViewQueryParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common.Features.GetUsersView; 2 | 3 | public sealed record GetUsersViewQueryParameters(DateTimeOffset RegistrationDate); 4 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUsersView/GetUsersViewQueryResponsePayload.cs: -------------------------------------------------------------------------------- 1 | using Twilight.Samples.Common.Views; 2 | 3 | namespace Twilight.Samples.Common.Features.GetUsersView; 4 | 5 | public sealed record GetUsersViewQueryResponsePayload(IEnumerable Users); 6 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/GetUsersView/GetUsersViewQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Queries; 3 | 4 | namespace Twilight.Samples.Common.Features.GetUsersView; 5 | 6 | public sealed class GetUsersViewQueryValidator : AbstractValidator>> 7 | { 8 | public GetUsersViewQueryValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.Params.RegistrationDate).LessThanOrEqualTo(DateTimeOffset.UtcNow); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/NotifyUserRegisteredCqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Events; 5 | 6 | namespace Twilight.Samples.Common.Features.RegisterUser; 7 | 8 | public sealed class NotifyUserRegisteredCqrsEventHandler( 9 | ILogger logger, 10 | IValidator> validator) : CqrsEventHandlerBase>(logger, validator) 11 | { 12 | public override async Task HandleEvent(CqrsEvent cqrsEvent, CancellationToken cancellationToken = default) 13 | { 14 | // Events can be used to trigger multiple business activities. 15 | Logger.LogInformation("Notify User Registered Handler: Handled Event, {EventTypeName}.", cqrsEvent.GetType().FullName); 16 | 17 | return await Task.FromResult(Result.Ok()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/RegisterUserCommandParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common.Features.RegisterUser; 2 | 3 | public sealed record RegisterUserCommandParameters(string Forename, string Surname); 4 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/RegisterUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Commands; 3 | 4 | namespace Twilight.Samples.Common.Features.RegisterUser; 5 | 6 | public sealed class RegisterUserCommandValidator : AbstractValidator> 7 | { 8 | public RegisterUserCommandValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.Params.Forename).NotNull().NotEmpty(); 12 | RuleFor(p => p.Params.Surname).NotNull().NotEmpty(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/RegisterUserCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using System.Diagnostics; 3 | using FluentValidation; 4 | using Microsoft.EntityFrameworkCore.ChangeTracking; 5 | using Microsoft.Extensions.Logging; 6 | using Twilight.CQRS.Commands; 7 | using Twilight.CQRS.Events; 8 | using Twilight.CQRS.Messaging.Interfaces; 9 | using Twilight.Samples.Common.Data; 10 | using Twilight.Samples.Common.Data.Entities; 11 | 12 | namespace Twilight.Samples.Common.Features.RegisterUser; 13 | 14 | public sealed class RegisterUserCqrsCommandHandler(SampleDataContext context, 15 | IMessageSender messageSender, 16 | ILogger logger, 17 | IValidator> validator) 18 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 19 | { 20 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 21 | { 22 | var userEntity = new UserEntity 23 | { 24 | Forename = command.Params.Forename, 25 | Surname = command.Params.Surname 26 | }; 27 | 28 | EntityEntry entityEntry; 29 | 30 | using (var activity = Activity.Current?.Source.StartActivity("Adding new user to database", ActivityKind.Server)) 31 | { 32 | activity?.AddEvent(new ActivityEvent("Register User")); 33 | 34 | entityEntry = context.Users.Add(userEntity); 35 | 36 | await context.SaveChangesAsync(cancellationToken); 37 | } 38 | 39 | var parameters = new UserRegisteredEventParameters(entityEntry.Entity.Id, command.Params.Forename, command.Params.Surname); 40 | var userRegisteredEvent = new CqrsEvent(parameters, command.CorrelationId, null, command.MessageId); 41 | 42 | Logger.LogInformation("Handled CQRS Command, {CommandTypeName}.", command.GetType().FullName); 43 | 44 | await MessageSender.Publish(userRegisteredEvent, cancellationToken); 45 | 46 | return Result.Ok(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/UserRegisteredCqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using System.Diagnostics; 3 | using FluentValidation; 4 | using Microsoft.Extensions.Logging; 5 | using Twilight.CQRS.Events; 6 | using Twilight.Samples.Common.Data; 7 | using Twilight.Samples.Common.Data.Entities; 8 | 9 | namespace Twilight.Samples.Common.Features.RegisterUser; 10 | 11 | public sealed class UserRegisteredCqrsEventHandler(ViewDataContext dataContext, 12 | ILogger logger, 13 | IValidator> validator) 14 | : CqrsEventHandlerBase>(logger, validator) 15 | { 16 | public override async Task HandleEvent(CqrsEvent cqrsEvent, CancellationToken cancellationToken = default) 17 | { 18 | var userViewEntity = new UserViewEntity 19 | { 20 | UserId = cqrsEvent.Params.UserId, 21 | Forename = cqrsEvent.Params.Forename, 22 | Surname = cqrsEvent.Params.Surname, 23 | FullName = $"{cqrsEvent.Params.Surname}, {cqrsEvent.Params.Forename}", 24 | RegistrationDate = DateTimeOffset.UtcNow 25 | }; 26 | 27 | using (var activity = Activity.Current?.Source.StartActivity("Adding new user to users view", ActivityKind.Server)) 28 | { 29 | activity?.AddEvent(new ActivityEvent("Add User to View")); 30 | 31 | dataContext.UsersView.Add(userViewEntity); 32 | } 33 | 34 | 35 | await dataContext.SaveChangesAsync(cancellationToken); 36 | 37 | Logger.LogInformation("User Registered Handler: Handled Event, {EventTypeName}.", cqrsEvent.GetType().FullName); 38 | 39 | return Result.Ok(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/UserRegisteredEventParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common.Features.RegisterUser; 2 | 3 | public sealed record UserRegisteredEventParameters(int UserId, string Forename, string Surname); 4 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/RegisterUser/UserRegisteredEventValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Events; 3 | 4 | namespace Twilight.Samples.Common.Features.RegisterUser; 5 | 6 | public sealed class UserRegisteredEventValidator : AbstractValidator> 7 | { 8 | public UserRegisteredEventValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.Params.Forename).NotNull().NotEmpty(); 12 | RuleFor(p => p.Params.Surname).NotNull().NotEmpty(); 13 | RuleFor(p => p.Params.UserId).NotNull().NotEmpty(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/IAssemblyMarker.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common; 2 | 3 | public interface IAssemblyMarker 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/IRunner.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common; 2 | 3 | public interface IRunner 4 | { 5 | Task Run(); 6 | } 7 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Twilight.Samples.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Views/UserView.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common.Views; 2 | 3 | public record UserView(int ViewId, int UserId, string Forename, string Surname, string FullName, DateTimeOffset? RegistrationDate); 4 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Autofac/ContainerBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Autofac; 3 | using CommunityToolkit.Diagnostics; 4 | using Twilight.CQRS.Interfaces; 5 | 6 | namespace Twilight.CQRS.Autofac; 7 | 8 | /// 9 | /// Provides extensions to the Autofac Container builder that allow for the easy registration of CQRS components from 10 | /// assemblies. 11 | /// 12 | public static class ContainerBuilderExtensions 13 | { 14 | /// 15 | /// Scans the specified assembly and registers types matching the specified endings against their implemented 16 | /// interfaces. 17 | /// 18 | /// All registrations will be made with instance per lifetime scope. 19 | /// The container builder. 20 | /// The assembly to scan. 21 | /// The file endings to match against. 22 | public static void RegisterAssemblyTypes(this ContainerBuilder builder, Assembly assembly, string[] typeNameEndings) 23 | { 24 | Guard.IsNotNull(assembly); 25 | Guard.IsNotNull(typeNameEndings); 26 | 27 | if (!typeNameEndings.Any()) 28 | { 29 | return; 30 | } 31 | 32 | foreach (var typeNameEnding in typeNameEndings) 33 | { 34 | builder.RegisterAssemblyTypes(assembly) 35 | .Where(type => type.Name.EndsWith(typeNameEnding, StringComparison.InvariantCultureIgnoreCase) && !type.IsAbstract) 36 | .AsImplementedInterfaces() 37 | .InstancePerLifetimeScope() 38 | .AsSelf(); 39 | } 40 | } 41 | 42 | /// 43 | /// Registers CQRS command, event, query handlers and message validators in the specified assembly. 44 | /// 45 | /// The container builder. 46 | /// The assemblies to scan. 47 | public static ContainerBuilder RegisterCqrs(this ContainerBuilder builder, IEnumerable assemblies) 48 | { 49 | var assemblyList = assemblies.ToList(); 50 | 51 | if (!assemblyList.Any()) 52 | { 53 | return builder; 54 | } 55 | 56 | assemblyList.ForEach(assembly => RegisterCqrs(builder, assembly)); 57 | 58 | return builder; 59 | } 60 | 61 | /// 62 | /// Registers CQRS command, event, query handlers and message validators in the specified assembly. 63 | /// 64 | /// The container builder. 65 | /// The assembly to scan. 66 | public static ContainerBuilder RegisterCqrs(this ContainerBuilder builder, Assembly assembly) 67 | { 68 | Guard.IsNotNull(assembly); 69 | 70 | builder.RegisterAssemblyTypes(assembly) 71 | .AsClosedTypesOf(typeof(ICqrsCommandHandler<>)); 72 | 73 | builder.RegisterAssemblyTypes(assembly) 74 | .AsClosedTypesOf(typeof(ICqrsCommandHandler<,>)); 75 | 76 | builder.RegisterAssemblyTypes(assembly) 77 | .AsClosedTypesOf(typeof(ICqrsQueryHandler<,>)); 78 | 79 | builder.RegisterAssemblyTypes(assembly) 80 | .AsClosedTypesOf(typeof(ICqrsEventHandler<>)); 81 | 82 | builder.RegisterAssemblyTypes(assembly, ["validator"]); 83 | 84 | return builder; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Autofac/Twilight.CQRS.Autofac.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | Twilight.CQRS.Autofac.xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Autofac/Twilight.CQRS.Autofac.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilight.CQRS.Autofac 5 | 6 | 7 | 8 | 9 | Provides extensions to the Autofac Container builder that allow for the easy registration of CQRS components from 10 | assemblies. 11 | 12 | 13 | 14 | 15 | Scans the specified assembly and registers types matching the specified endings against their implemented 16 | interfaces. 17 | 18 | All registrations will be made with instance per lifetime scope. 19 | The container builder. 20 | The assembly to scan. 21 | The file endings to match against. 22 | 23 | 24 | 25 | Registers CQRS command, event, query handlers and message validators in the specified assembly. 26 | 27 | The container builder. 28 | The assemblies to scan. 29 | 30 | 31 | 32 | Registers CQRS command, event, query handlers and message validators in the specified assembly. 33 | 34 | The container builder. 35 | The assembly to scan. 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Interfaces; 2 | 3 | /// 4 | /// Represents a message of type command. 5 | /// Implements . 6 | /// 7 | /// 8 | public interface ICqrsCommand : ICqrsMessage 9 | { 10 | } 11 | 12 | /// 13 | /// Represents a message of type command with a response of arbitrary type. 14 | /// Implements . 15 | /// 16 | /// The type of the response payload. 17 | /// 18 | public interface ICqrsCommand : ICqrsMessage 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Interfaces; 4 | 5 | /// 6 | /// Represents a means of handling a command in order to broker a result. 7 | /// 8 | /// The type of the command. 9 | public interface ICqrsCommandHandler : ICqrsMessageHandler 10 | where TCommand : ICqrsCommand 11 | { 12 | /// 13 | /// Handles the command. 14 | /// 15 | /// The command. 16 | /// The cancellation token. 17 | /// A task that represents the asynchronous command handler operation. 18 | Task Handle(TCommand command, CancellationToken cancellationToken = default); 19 | } 20 | 21 | /// 22 | /// Represents a command message handler. 23 | /// 24 | /// The type of the command. 25 | /// The type of the response. 26 | public interface ICqrsCommandHandler : ICqrsMessageHandler 27 | where TCommand : class, ICqrsCommand 28 | where TResponse : class 29 | { 30 | /// 31 | /// Handles the command. 32 | /// 33 | /// The command. 34 | /// The cancellation token. 35 | /// 36 | /// A task that represents the asynchronous command handler operation. 37 | /// The task result contains the command execution response. 38 | /// 39 | Task> Handle(TCommand command, CancellationToken cancellationToken = default); 40 | } 41 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Interfaces; 2 | 3 | /// 4 | /// Represents a message of type event. 5 | /// Implements . 6 | /// 7 | /// 8 | public interface ICqrsEvent : ICqrsMessage 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Interfaces; 4 | 5 | /// 6 | /// Represents a means of handling an event in order to broker a result. 7 | /// 8 | /// The type of the event. 9 | public interface ICqrsEventHandler : ICqrsMessageHandler 10 | where TEvent : ICqrsEvent 11 | { 12 | /// 13 | /// Handles the event. 14 | /// 15 | /// The event. 16 | /// The cancellation token. 17 | /// A task that represents the asynchronous event handler operation. 18 | Task Handle(TEvent @event, CancellationToken cancellationToken = default); 19 | } 20 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Interfaces; 2 | 3 | /// 4 | /// Represents base properties for messages. This class cannot be instantiated. 5 | /// 6 | public interface ICqrsMessage 7 | { 8 | /// 9 | /// Gets the message identifier. 10 | /// 11 | /// The message identifier. 12 | string MessageId { get; } 13 | 14 | /// 15 | /// Gets the correlation identifier. 16 | /// 17 | /// The message correlation identifier. 18 | string CorrelationId { get; } 19 | 20 | /// 21 | /// Gets the session identifier. 22 | /// 23 | /// The session identifier. 24 | string? SessionId { get; } 25 | 26 | /// 27 | /// Gets the causation identifier. 28 | /// 29 | /// Identifies the message (by that message's identifier) that caused a message instance to be produced. 30 | /// The causation identifier. 31 | string? CausationId { get; } 32 | } 33 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Interfaces; 4 | 5 | /// 6 | /// Represents base message handling functionality. This class cannot be inherited. 7 | /// 8 | /// The type of the message. 9 | public interface ICqrsMessageHandler 10 | { 11 | /// 12 | /// Occurs before handling a message. 13 | /// 14 | /// The message. 15 | /// The cancellation token. 16 | /// A task that represents the asynchronous operation. 17 | Task OnBeforeHandling(TMessage message, CancellationToken cancellationToken = default); 18 | 19 | /// 20 | /// Occurs when validating a message. 21 | /// 22 | /// The message to be validated. 23 | /// The cancellation token. 24 | /// A task that represents the asynchronous operation. 25 | Task ValidateMessage(TMessage message, CancellationToken cancellationToken = default); 26 | 27 | /// 28 | /// Occurs when handling a message has completed. 29 | /// 30 | /// The message. 31 | /// The cancellation token. 32 | /// A task that represents the asynchronous operation. 33 | Task OnAfterHandling(TMessage message, CancellationToken cancellationToken = default); 34 | } 35 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Interfaces; 2 | 3 | /// 4 | /// Represents a message of type query with a response of arbitrary type. 5 | /// Implements . 6 | /// 7 | /// The type of the response payload. 8 | /// 9 | public interface ICqrsQuery : ICqrsMessage 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Interfaces; 4 | 5 | /// 6 | /// Represents a query message handler. 7 | /// 8 | /// The type of the query. 9 | /// The type of the response. 10 | public interface ICqrsQueryHandler : ICqrsMessageHandler 11 | where TQuery : class, ICqrsQuery 12 | { 13 | /// 14 | /// Handles the query. 15 | /// 16 | /// The query. 17 | /// The cancellation token. 18 | /// 19 | /// A task that represents the asynchronous query handler operation. 20 | /// The task result contains the query execution response. 21 | /// 22 | Task> Handle(TQuery query, CancellationToken cancellationToken = default); 23 | } 24 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/Twilight.CQRS.Interfaces.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | Twilight.CQRS.Interfaces.xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/Twilight.CQRS.Interfaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilight.CQRS.Interfaces 5 | 6 | 7 | 8 | 9 | Represents a message of type command. 10 | Implements . 11 | 12 | 13 | 14 | 15 | 16 | Represents a message of type command with a response of arbitrary type. 17 | Implements . 18 | 19 | The type of the response payload. 20 | 21 | 22 | 23 | 24 | Represents a means of handling a command in order to broker a result. 25 | 26 | The type of the command. 27 | 28 | 29 | 30 | Handles the command. 31 | 32 | The command. 33 | The cancellation token. 34 | A task that represents the asynchronous command handler operation. 35 | 36 | 37 | 38 | Represents a command message handler. 39 | 40 | The type of the command. 41 | The type of the response. 42 | 43 | 44 | 45 | Handles the command. 46 | 47 | The command. 48 | The cancellation token. 49 | 50 | A task that represents the asynchronous command handler operation. 51 | The task result contains the command execution response. 52 | 53 | 54 | 55 | 56 | Represents a message of type event. 57 | Implements . 58 | 59 | 60 | 61 | 62 | 63 | Represents a means of handling an event in order to broker a result. 64 | 65 | The type of the event. 66 | 67 | 68 | 69 | Handles the event. 70 | 71 | The event. 72 | The cancellation token. 73 | A task that represents the asynchronous event handler operation. 74 | 75 | 76 | 77 | Represents base properties for messages. This class cannot be instantiated. 78 | 79 | 80 | 81 | 82 | Gets the message identifier. 83 | 84 | The message identifier. 85 | 86 | 87 | 88 | Gets the correlation identifier. 89 | 90 | The message correlation identifier. 91 | 92 | 93 | 94 | Gets the session identifier. 95 | 96 | The session identifier. 97 | 98 | 99 | 100 | Gets the causation identifier. 101 | 102 | Identifies the message (by that message's identifier) that caused a message instance to be produced. 103 | The causation identifier. 104 | 105 | 106 | 107 | Represents base message handling functionality. This class cannot be inherited. 108 | 109 | The type of the message. 110 | 111 | 112 | 113 | Occurs before handling a message. 114 | 115 | The message. 116 | The cancellation token. 117 | A task that represents the asynchronous operation. 118 | 119 | 120 | 121 | Occurs when validating a message. 122 | 123 | The message to be validated. 124 | The cancellation token. 125 | A task that represents the asynchronous operation. 126 | 127 | 128 | 129 | Occurs when handling a message has completed. 130 | 131 | The message. 132 | The cancellation token. 133 | A task that represents the asynchronous operation. 134 | 135 | 136 | 137 | Represents a message of type query with a response of arbitrary type. 138 | Implements . 139 | 140 | The type of the response payload. 141 | 142 | 143 | 144 | 145 | Represents a query message handler. 146 | 147 | The type of the query. 148 | The type of the response. 149 | 150 | 151 | 152 | Handles the query. 153 | 154 | The query. 155 | The cancellation token. 156 | 157 | A task that represents the asynchronous query handler operation. 158 | The task result contains the query execution response. 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.InMemory.Autofac/AutofacInMemoryMessageSender.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Reflection; 3 | using Autofac; 4 | using Autofac.Core; 5 | using Autofac.Core.Registration; 6 | using Microsoft.Extensions.Logging; 7 | using CommunityToolkit.Diagnostics; 8 | using FluentResults; 9 | using Twilight.CQRS.Interfaces; 10 | using Twilight.CQRS.Messaging.Interfaces; 11 | 12 | namespace Twilight.CQRS.Messaging.InMemory.Autofac; 13 | 14 | /// 15 | /// 16 | /// Provides a means of dispatching messages. This implementation uses Autofac to resolve a registered message 17 | /// handler from the container and call that handler, passing any appropriate message. This class cannot be 18 | /// inherited. 19 | /// 20 | /// Implements . 21 | /// 22 | /// 23 | public sealed class AutofacInMemoryMessageSender : IMessageSender 24 | { 25 | private const string DefaultAssemblyVersion = "1.0.0.0"; 26 | 27 | private readonly ILifetimeScope _lifetimeScope; 28 | private readonly ILogger _logger; 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// The Autofac lifetime scope. 34 | /// The logger. 35 | public AutofacInMemoryMessageSender(ILifetimeScope lifetimeScope, ILogger logger) 36 | { 37 | Guard.IsNotNull(lifetimeScope); 38 | Guard.IsNotNull(logger); 39 | 40 | _lifetimeScope = lifetimeScope; 41 | _logger = logger; 42 | } 43 | 44 | private static string Namespace => typeof(AutofacInMemoryMessageSender).Namespace ?? nameof(AutofacInMemoryMessageSender); 45 | 46 | private static string AssemblyVersion => Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? DefaultAssemblyVersion; 47 | 48 | /// 49 | public async Task Send(TCommand command, CancellationToken cancellationToken = default) 50 | where TCommand : class, ICqrsCommand 51 | { 52 | var guardResult = Result.Try(() => 53 | { 54 | Guard.IsNotNull(command); 55 | }); 56 | 57 | if (guardResult.IsFailed) 58 | { 59 | return guardResult; 60 | } 61 | 62 | var activitySource = new ActivitySource(Namespace, AssemblyVersion); 63 | 64 | using var activity = activitySource.StartActivity($"Send {command.GetType()}"); 65 | { 66 | await using var scope = _lifetimeScope.BeginLifetimeScope(); 67 | 68 | var assemblyQualifiedName = typeof(ICqrsCommandHandler).AssemblyQualifiedName ?? "Unknown Assembly"; 69 | 70 | IEnumerable>? handlers; 71 | 72 | try 73 | { 74 | _lifetimeScope.TryResolve(out handlers); 75 | } 76 | catch (DependencyResolutionException ex) 77 | { 78 | _logger.LogCritical(ex, $"No concrete handlers for type '{assemblyQualifiedName}' could be resolved."); 79 | 80 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 81 | } 82 | 83 | var commandHandlers = (handlers ?? Array.Empty>()).ToList(); 84 | 85 | switch (commandHandlers.Count) 86 | { 87 | case 0: 88 | 89 | _logger.LogCritical("Handler not found in '{AssemblyQualifiedName}'.", assemblyQualifiedName); 90 | 91 | return Result.Fail("No handler cold be found for this request."); 92 | 93 | case > 1: 94 | 95 | _logger.LogCritical("Multiple handlers found in {AssemblyQualifiedName}.", assemblyQualifiedName); 96 | 97 | return Result.Fail("Multiple handlers found. A command may only have one handler."); 98 | 99 | default: 100 | 101 | await commandHandlers[0].Handle(command, cancellationToken); 102 | break; 103 | } 104 | } 105 | 106 | return Result.Ok(); 107 | } 108 | 109 | /// 110 | public async Task> Send(ICqrsCommand command, CancellationToken cancellationToken = default) 111 | { 112 | var guardResult = Result.Try(() => 113 | { 114 | Guard.IsNotNull(command); 115 | }); 116 | 117 | if (guardResult.IsFailed) 118 | { 119 | return guardResult; 120 | } 121 | 122 | var genericType = typeof(ICqrsCommandHandler<,>); 123 | var closedGenericType = genericType.MakeGenericType(command.GetType(), typeof(TResult)); 124 | var assemblyQualifiedName = closedGenericType.AssemblyQualifiedName ?? "Unknown Assembly"; 125 | 126 | await using var scope = _lifetimeScope.BeginLifetimeScope(); 127 | 128 | var activitySource = new ActivitySource(Namespace, AssemblyVersion); 129 | 130 | using var activity = activitySource.StartActivity($"Send {command.GetType()}"); 131 | { 132 | object result; 133 | 134 | try 135 | { 136 | var handlerExists = _lifetimeScope.TryResolve(closedGenericType, out var handler); 137 | 138 | if (!handlerExists) 139 | { 140 | _logger.LogCritical("Handler not found in '{AssemblyQualifiedName}'.", assemblyQualifiedName); 141 | 142 | return Result.Fail("No handler cold be found for this request."); 143 | } 144 | 145 | var handlerType = handler!.GetType(); 146 | var handlerTypeRuntimeMethod = handlerType.GetRuntimeMethod("Handle", [command.GetType(), typeof(CancellationToken)]) 147 | ?? throw new InvalidOperationException($"Failed to get runtime method 'Handle' from {handlerType}."); 148 | 149 | var resultTask = (Task>)handlerTypeRuntimeMethod.Invoke(handler, [command, cancellationToken])!; 150 | 151 | result = await resultTask; 152 | } 153 | catch (DependencyResolutionException ex) 154 | { 155 | _logger.LogCritical(ex, $"No concrete handlers for type '{assemblyQualifiedName}' could be resolved."); 156 | 157 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 158 | } 159 | catch (InvalidOperationException ex) 160 | { 161 | _logger.LogCritical(ex, $"Could not execute handler for '{assemblyQualifiedName}'."); 162 | 163 | return Result.Fail("Failed to execute handler."); 164 | } 165 | 166 | return (Result)result; 167 | } 168 | } 169 | 170 | /// 171 | public async Task> Send(ICqrsQuery query, CancellationToken cancellationToken = default) 172 | { 173 | var guardResult = Result.Try(() => 174 | { 175 | Guard.IsNotNull(query); 176 | }); 177 | 178 | if (guardResult.IsFailed) 179 | { 180 | return guardResult; 181 | } 182 | 183 | var genericType = typeof(ICqrsQueryHandler<,>); 184 | var closedGenericType = genericType.MakeGenericType(query.GetType(), typeof(TResult)); 185 | var assemblyQualifiedName = closedGenericType.AssemblyQualifiedName ?? "Unknown Assembly"; 186 | 187 | await using var scope = _lifetimeScope.BeginLifetimeScope(); 188 | 189 | var activitySource = new ActivitySource(Namespace, AssemblyVersion); 190 | 191 | using var activity = activitySource.StartActivity($"Send {query.GetType()}"); 192 | { 193 | object result; 194 | 195 | try 196 | { 197 | var handlerExists = _lifetimeScope.TryResolve(closedGenericType, out var handler); 198 | 199 | if (!handlerExists) 200 | { 201 | _logger.LogCritical("Handler not found in '{AssemblyQualifiedName}'.", assemblyQualifiedName); 202 | 203 | return Result.Fail("No handler cold be found for this request."); 204 | } 205 | 206 | var handlerType = handler!.GetType(); 207 | var handlerTypeRuntimeMethod = handlerType.GetRuntimeMethod("Handle", [query.GetType(), typeof(CancellationToken)]) 208 | ?? throw new InvalidOperationException($"Failed to get runtime method 'Handle' from {handlerType}."); 209 | 210 | var resultTask = (Task>)handlerTypeRuntimeMethod.Invoke(handler, [query, cancellationToken])!; 211 | 212 | result = await resultTask; 213 | } 214 | catch (DependencyResolutionException ex) 215 | { 216 | _logger.LogCritical(ex, $"No concrete handlers for type '{assemblyQualifiedName}' could be resolved."); 217 | 218 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 219 | } 220 | 221 | return (Result)result; 222 | } 223 | } 224 | 225 | /// 226 | public async Task Publish(IEnumerable events, CancellationToken cancellationToken = default) 227 | where TEvent : class, ICqrsEvent 228 | { 229 | var eventsList = events as TEvent[] ?? events.ToArray(); 230 | 231 | if (!eventsList.Length.Equals(0)) 232 | { 233 | _logger.LogWarning("No events received for publishing when at least one event was expected. Check calls to publish."); 234 | } 235 | 236 | foreach (var @event in eventsList) 237 | { 238 | await Publish(@event, cancellationToken); 239 | } 240 | 241 | return Result.Ok(); 242 | } 243 | 244 | /// 245 | public async Task Publish(TEvent @event, CancellationToken cancellationToken = default) 246 | where TEvent : class, ICqrsEvent 247 | { 248 | var guardResult = Result.Try(() => 249 | { 250 | Guard.IsNotNull(@event); 251 | }); 252 | 253 | if (guardResult.IsFailed) 254 | { 255 | return guardResult; 256 | } 257 | 258 | var activitySource = new ActivitySource(Namespace, AssemblyVersion); 259 | 260 | using var activity = activitySource.StartActivity($"Publish {@event.GetType()}"); 261 | { 262 | await using var scope = _lifetimeScope.BeginLifetimeScope(); 263 | 264 | var assemblyQualifiedName = typeof(ICqrsEventHandler).AssemblyQualifiedName ?? "Unknown Assembly"; 265 | 266 | IEnumerable> handlers; 267 | 268 | try 269 | { 270 | handlers = _lifetimeScope.Resolve>>() 271 | .ToList(); 272 | } 273 | catch (ComponentNotRegisteredException ex) 274 | { 275 | _logger.LogCritical(ex, $"No concrete handlers for type '{assemblyQualifiedName}' could be found."); 276 | 277 | return Result.Fail("A component is not registered in the DI container. Check your component is registered."); 278 | } 279 | catch (DependencyResolutionException ex) 280 | { 281 | _logger.LogCritical(ex, $"No concrete handlers for type '{assemblyQualifiedName}' could be resolved."); 282 | 283 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 284 | } 285 | 286 | if (!handlers.Any()) 287 | { 288 | _logger.LogCritical("Handler not found in '{AssemblyQualifiedName}'.", assemblyQualifiedName); 289 | 290 | return Result.Fail("No handler cold be found for this request."); 291 | } 292 | 293 | var tasks = new List(handlers.Count()); 294 | 295 | tasks.AddRange(handlers.Select(handler => handler.Handle(@event, cancellationToken))); 296 | 297 | await Task.WhenAll(tasks); 298 | } 299 | 300 | return Result.Ok(); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.InMemory.Autofac/AutofacInMemoryMessagingRegistrationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Twilight.CQRS.Messaging.Interfaces; 3 | 4 | namespace Twilight.CQRS.Messaging.InMemory.Autofac; 5 | 6 | /// 7 | /// Provides an extension that uses Autofac to register in-memory messaging. 8 | /// 9 | public static class AutofacInMemoryMessagingRegistrationExtensions 10 | { 11 | /// 12 | /// Adds in-memory messaging using Autofac. 13 | /// 14 | /// The component registration builder. 15 | /// ContainerBuilder. 16 | public static ContainerBuilder AddAutofacInMemoryMessaging(this ContainerBuilder builder) 17 | { 18 | builder.RegisterType().As(); 19 | 20 | return builder; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.InMemory.Autofac/Twilight.CQRS.Messaging.InMemory.Autofac.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | Twilight.CQRS.Messaging.InMemory.Autofac.xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.InMemory.Autofac/Twilight.CQRS.Messaging.InMemory.Autofac.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilight.CQRS.Messaging.InMemory.Autofac 5 | 6 | 7 | 8 | 9 | 10 | Provides a means of dispatching messages. This implementation uses Autofac to resolve a registered message 11 | handler from the container and call that handler, passing any appropriate message. This class cannot be 12 | inherited. 13 | 14 | Implements . 15 | 16 | 17 | 18 | 19 | 20 | Initializes a new instance of the class. 21 | 22 | The Autofac lifetime scope. 23 | The logger. 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Provides an extension that uses Autofac to register in-memory messaging. 43 | 44 | 45 | 46 | 47 | Adds in-memory messaging using Autofac. 48 | 49 | The component registration builder. 50 | ContainerBuilder. 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.Interfaces/IMessageSender.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using Twilight.CQRS.Interfaces; 3 | 4 | namespace Twilight.CQRS.Messaging.Interfaces; 5 | 6 | /// 7 | /// Represents a means of dispatching messages. 8 | /// 9 | public interface IMessageSender 10 | { 11 | /// 12 | /// Runs the command handler registered for the given command type. 13 | /// 14 | /// Type of the command. 15 | /// Instance of the command. 16 | /// Task cancellation token. 17 | /// A Task that completes when the handler finished processing. 18 | Task Send(TCommand command, CancellationToken cancellationToken = default) 19 | where TCommand : class, ICqrsCommand; 20 | 21 | /// 22 | /// Runs the command handler registered for the given command type. 23 | /// 24 | /// 25 | /// This method should be implemented when a response (reply) to the originating service is required (i.e. the 26 | /// result of the command is fulfilled). It is recommended to restrain a command response to a scalar value. 27 | /// 28 | /// Type of the result. 29 | /// Instance of the command. 30 | /// Task cancellation token. 31 | /// A Task that resolves to a result of the command handler. 32 | Task> Send(ICqrsCommand command, CancellationToken cancellationToken = default); 33 | 34 | /// 35 | /// Runs the query handler registered for the given query type. 36 | /// 37 | /// 38 | /// This method should be implemented when a response (reply) to the originating service is required (i.e. the 39 | /// result of the query is fulfilled). 40 | /// 41 | /// Type of the result. 42 | /// Instance of the query. 43 | /// Task cancellation token. 44 | /// A Task that resolves to a result of the query handler. 45 | Task> Send(ICqrsQuery query, CancellationToken cancellationToken = default); 46 | 47 | /// 48 | /// Runs all registered event handlers for the specified events. 49 | /// 50 | /// The domain events. 51 | /// Task cancellation token. 52 | /// Task that completes when all handlers finish processing. 53 | Task Publish(IEnumerable events, CancellationToken cancellationToken = default) 54 | where TEvent : class, ICqrsEvent; 55 | 56 | /// 57 | /// Runs all registered event handlers for the specified event. 58 | /// 59 | /// Type of the event. 60 | /// Instance of the event. 61 | /// Task cancellation token. 62 | /// A Task that completes when all handlers finish processing. 63 | Task Publish(TEvent @event, CancellationToken cancellationToken = default) 64 | where TEvent : class, ICqrsEvent; 65 | } 66 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.Interfaces/Twilight.CQRS.Messaging.Interfaces.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | Twilight.CQRS.Messaging.Interfaces.xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.Interfaces/Twilight.CQRS.Messaging.Interfaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilight.CQRS.Messaging.Interfaces 5 | 6 | 7 | 8 | 9 | Represents a means of dispatching messages. 10 | 11 | 12 | 13 | 14 | Runs the command handler registered for the given command type. 15 | 16 | Type of the command. 17 | Instance of the command. 18 | Task cancellation token. 19 | A Task that completes when the handler finished processing. 20 | 21 | 22 | 23 | Runs the command handler registered for the given command type. 24 | 25 | 26 | This method should be implemented when a response (reply) to the originating service is required (i.e. the 27 | result of the command is fulfilled). It is recommended to restrain a command response to a scalar value. 28 | 29 | Type of the result. 30 | Instance of the command. 31 | Task cancellation token. 32 | A Task that resolves to a result of the command handler. 33 | 34 | 35 | 36 | Runs the query handler registered for the given query type. 37 | 38 | 39 | This method should be implemented when a response (reply) to the originating service is required (i.e. the 40 | result of the query is fulfilled). 41 | 42 | Type of the result. 43 | Instance of the query. 44 | Task cancellation token. 45 | A Task that resolves to a result of the query handler. 46 | 47 | 48 | 49 | Runs all registered event handlers for the specified events. 50 | 51 | The domain events. 52 | Task cancellation token. 53 | Task that completes when all handlers finish processing. 54 | 55 | 56 | 57 | Runs all registered event handlers for the specified event. 58 | 59 | Type of the event. 60 | Instance of the event. 61 | Task cancellation token. 62 | A Task that completes when all handlers finish processing. 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Commands/CqrsCommand.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Interfaces; 2 | 3 | namespace Twilight.CQRS.Commands; 4 | 5 | /// 6 | /// 7 | /// Represents an action that does something and may carry parameters as a payload or not. Irrespective of 8 | /// whether a command has parameters or not, a command is always a 'fire-and-forget' operation and therefore should 9 | /// not return a response. 10 | /// 11 | /// Implements . 12 | /// Implements . 13 | /// 14 | /// 15 | /// 16 | public class CqrsCommand : CqrsMessage, ICqrsCommand 17 | { 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The command correlation identifier. 22 | /// The session identifier. 23 | /// 24 | /// The causation identifier. Identifies the message that caused this command to be produced. 25 | /// Optional. 26 | /// 27 | public CqrsCommand(string correlationId, string? causationId = null, string? sessionId = null) 28 | : base(correlationId, sessionId, causationId) 29 | { 30 | } 31 | } 32 | 33 | /// 34 | /// 35 | /// Represents an action that does something and may carry a payload of arbitrary type 36 | /// . The command may carry parameters as a payload or not. Irrespective of 37 | /// whether a command has parameters or not, a command is always a 'fire-and-forget' operation and therefore should 38 | /// not return a response. 39 | /// 40 | /// Implements . 41 | /// Implements . 42 | /// 43 | /// The type of the parameters. 44 | /// 45 | /// 46 | public class CqrsCommand : CqrsMessage, ICqrsCommand 47 | where TParameters : class 48 | { 49 | /// 50 | /// Initializes a new instance of the class. 51 | /// 52 | /// The typed command parameters. 53 | /// The command correlation identifier. 54 | /// The session identifier. 55 | /// 56 | /// The causation identifier. Identifies the message that caused this command to be produced. 57 | /// Optional. 58 | /// 59 | public CqrsCommand(TParameters parameters, string correlationId, string? sessionId = null, string? causationId = null) 60 | : base(correlationId, sessionId, causationId) 61 | => Params = parameters; 62 | 63 | /// 64 | /// Gets the typed command parameters. 65 | /// 66 | /// The parameters. 67 | public TParameters Params { get; } 68 | } 69 | 70 | /// 71 | /// 72 | /// Represents a result and does not change the observable state of the system (i.e. is free of side effects). 73 | /// 74 | /// Implements . 75 | /// Implements . 76 | /// 77 | /// The type of the parameters. 78 | /// The type of the response. 79 | /// 80 | /// 81 | public class CqrsCommand : CqrsMessage, ICqrsCommand 82 | where TParameters : class 83 | where TResponse : class 84 | { 85 | /// 86 | /// Initializes a new instance of the class. 87 | /// 88 | /// The parameters. 89 | /// The command correlation identifier. 90 | /// The session identifier. 91 | /// 92 | /// The causation identifier. Identifies the message that caused this command to be produced. 93 | /// Optional. 94 | /// 95 | public CqrsCommand(TParameters parameters, string correlationId, string? sessionId = null, string? causationId = null) 96 | : base(correlationId, sessionId, causationId) 97 | => Params = parameters; 98 | 99 | /// 100 | /// Gets the typed command parameters. 101 | /// 102 | /// The parameters. 103 | public TParameters Params { get; } 104 | } 105 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Commands/CqrsCommandHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using CommunityToolkit.Diagnostics; 4 | using FluentResults; 5 | using FluentValidation; 6 | using Twilight.CQRS.Interfaces; 7 | using Twilight.CQRS.Messaging.Interfaces; 8 | // ReSharper disable ExplicitCallerInfoArgument as false positive for StartActivity 9 | 10 | namespace Twilight.CQRS.Commands; 11 | 12 | /// 13 | /// 14 | /// Represents the ability to process (handle) commands. A command handler receives a command and brokers a result. 15 | /// A result is either a successful application of the command, or an exception. This class cannot be instantiated. 16 | /// 17 | /// Implements . 18 | /// Implements . 19 | /// 20 | /// The type of the command. 21 | /// The type of the command handler. 22 | /// 23 | /// 24 | public abstract class CqrsCommandHandlerBase : CqrsMessageHandler, ICqrsCommandHandler 25 | where TCommand : class, ICqrsCommand 26 | { 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The message sender. 31 | /// The logger. 32 | /// The command validator. 33 | protected CqrsCommandHandlerBase(IMessageSender messageSender, ILogger logger, IValidator? validator = default) 34 | : base(logger, validator) 35 | { 36 | Guard.IsNotNull(messageSender); 37 | 38 | MessageSender = messageSender; 39 | } 40 | 41 | /// 42 | /// Gets the message sender. 43 | /// 44 | /// The message sender. 45 | protected IMessageSender MessageSender { get; } 46 | 47 | /// 48 | public async Task Handle(TCommand command, CancellationToken cancellationToken = default) 49 | { 50 | var guardResult = Result.Try(() => 51 | { 52 | Guard.IsNotNull(command); 53 | }); 54 | 55 | if (guardResult.IsFailed) 56 | { 57 | return guardResult; 58 | } 59 | 60 | using var activity = Activity.Current?.Source.StartActivity($"Handle {command.GetType()}"); 61 | { 62 | using (var childSpan = Activity.Current?.Source.StartActivity("Pre command handling actions")) 63 | { 64 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(OnBeforeHandling)}")); 65 | 66 | var result = await OnBeforeHandling(command, cancellationToken); 67 | 68 | if (!result.IsSuccess) 69 | { 70 | return result; 71 | } 72 | } 73 | 74 | using (var childSpan = Activity.Current?.Source.StartActivity("Validate command")) 75 | { 76 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(ValidateMessage)}")); 77 | 78 | var result = await ValidateMessage(command, cancellationToken); 79 | 80 | if (!result.IsSuccess) 81 | { 82 | return result; 83 | } 84 | } 85 | 86 | using (var childSpan = Activity.Current?.Source.StartActivity("Handle command")) 87 | { 88 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(HandleCommand)}")); 89 | 90 | var result = await HandleCommand(command, cancellationToken); 91 | 92 | if (!result.IsSuccess) 93 | { 94 | return result; 95 | } 96 | } 97 | 98 | using (var childSpan = Activity.Current?.Source.StartActivity("Post command handling actions")) 99 | { 100 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(OnAfterHandling)}")); 101 | 102 | var result = await OnAfterHandling(command, cancellationToken); 103 | 104 | if (!result.IsSuccess) 105 | { 106 | return result; 107 | } 108 | } 109 | } 110 | 111 | return Result.Ok(); 112 | } 113 | 114 | /// 115 | /// Handles the command. 116 | /// 117 | /// The command. 118 | /// The cancellation token. 119 | /// A task that represents the asynchronous handle command operation. 120 | public abstract Task HandleCommand(TCommand command, CancellationToken cancellationToken = default); 121 | } 122 | 123 | /// 124 | /// 125 | /// Represents the ability to process (handle) commands that return a scalar response. A command handler receives a 126 | /// command and directs the command for processing. This class cannot be instantiated. 127 | /// 128 | /// Implements . 129 | /// Implements . 130 | /// 131 | /// The type of the command. 132 | /// The type of the command response. 133 | /// The type of the command handler. 134 | /// 135 | /// 136 | public abstract class CqrsCommandHandlerBase : CqrsMessageHandler, ICqrsCommandHandler 137 | where TCommand : class, ICqrsCommand 138 | where TResponse : class,ICqrsMessage 139 | { 140 | /// 141 | /// Initializes a new instance of the class. 142 | /// 143 | /// The message sender. 144 | /// The logger. 145 | /// The command validator. 146 | protected CqrsCommandHandlerBase(IMessageSender messageSender, ILogger logger, IValidator? validator = default) 147 | : base(logger, validator) 148 | => MessageSender = messageSender; 149 | 150 | /// 151 | /// Gets the message sender. 152 | /// 153 | /// The message sender. 154 | protected IMessageSender MessageSender { get; } 155 | 156 | /// 157 | public async Task> Handle(TCommand command, CancellationToken cancellationToken = default) 158 | { 159 | var guardResult = Result.Try(() => 160 | { 161 | Guard.IsNotNull(command); 162 | }); 163 | 164 | if (guardResult.IsFailed) 165 | { 166 | return guardResult; 167 | } 168 | 169 | Result commandResult; 170 | 171 | using var activity = Activity.Current?.Source.StartActivity($"Handle {command.GetType()}"); 172 | { 173 | using (var childSpan = Activity.Current?.Source.StartActivity("pre command handling actions")) 174 | { 175 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(OnBeforeHandling)}")); 176 | 177 | var result = await OnBeforeHandling(command, cancellationToken); 178 | 179 | if (!result.IsSuccess) 180 | { 181 | return result; 182 | } 183 | } 184 | 185 | using (var childSpan = Activity.Current?.Source.StartActivity("Validate command")) 186 | { 187 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(ValidateMessage)}")); 188 | 189 | var result = await ValidateMessage(command, cancellationToken); 190 | 191 | if (!result.IsSuccess) 192 | { 193 | return result; 194 | } 195 | } 196 | 197 | using (var childSpan = Activity.Current?.Source.StartActivity("Handle command")) 198 | { 199 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(HandleCommand)}")); 200 | 201 | var result = await HandleCommand(command, cancellationToken); 202 | 203 | if (!result.IsSuccess) 204 | { 205 | return result; 206 | } 207 | 208 | commandResult = result; 209 | } 210 | 211 | using (var childSpan = Activity.Current?.Source.StartActivity("Post command handling actions")) 212 | { 213 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase)}.{nameof(OnAfterHandling)}")); 214 | 215 | var result = await OnAfterHandling(command, cancellationToken); 216 | 217 | if (!result.IsSuccess) 218 | { 219 | return result; 220 | } 221 | } 222 | } 223 | 224 | return commandResult; 225 | } 226 | 227 | /// 228 | /// Handles the command. 229 | /// 230 | /// The command. 231 | /// The cancellation token. 232 | /// 233 | /// A task that represents the asynchronous command handler operation. 234 | /// The task result contains the command execution response. 235 | /// 236 | protected abstract Task> HandleCommand(TCommand command, CancellationToken cancellationToken = default); 237 | } 238 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Commands/CqrsCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Commands; 2 | 3 | /// 4 | /// Represents an encapsulated response from a command handler. 5 | /// Implements . 6 | /// 7 | /// 8 | /// 9 | /// Strictly speaking, a command should never return a value, however this response type allows for the scalar 10 | /// return of a command outcome, for example the rows affected for a database update or the identifier for a newly 11 | /// created data record. 12 | /// 13 | /// 14 | /// The design of this response intentionally restricts the type of value that can be returned to structs like int 15 | /// and Guid. For more complex objects, the standard Query / Query response should be used. 16 | /// 17 | /// 18 | /// Beware of using this approach with distributed commands. They really have to be true fire-and-forget 19 | /// operations. 20 | /// 21 | /// 22 | /// The type of the payload. 23 | /// 24 | public class CqrsCommandResponse : CqrsMessage 25 | where TPayload : class 26 | { 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The payload. 31 | /// The message correlation identifier. 32 | /// 33 | /// The causation identifier. Identifies the query that caused this response to be produced. 34 | /// Optional. 35 | /// 36 | public CqrsCommandResponse(TPayload payload, string correlationId, string? causationId = null) 37 | : base(correlationId, causationId) 38 | => Payload = payload; 39 | 40 | /// 41 | /// Gets the typed query response payload. 42 | /// 43 | /// The payload. 44 | public TPayload Payload { get; } 45 | } 46 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/CqrsMessage.cs: -------------------------------------------------------------------------------- 1 | using Taikandi; 2 | using Twilight.CQRS.Interfaces; 3 | 4 | namespace Twilight.CQRS; 5 | 6 | /// 7 | public abstract class CqrsMessage : ICqrsMessage 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The message correlation identifier. 13 | /// The session identifier for the session to which this message belongs. 14 | /// 15 | /// The causation identifier. Identifies the message that caused this message to be produced. 16 | /// Optional. 17 | /// 18 | protected CqrsMessage(string correlationId, string? sessionId = null, string? causationId = null) 19 | { 20 | MessageId = SequentialGuid.NewGuid().ToString(); 21 | CausationId = causationId; 22 | SessionId = sessionId; 23 | CorrelationId = correlationId; 24 | } 25 | 26 | /// 27 | public string MessageId { get; } 28 | 29 | /// 30 | public string CorrelationId { get; } 31 | 32 | /// 33 | public string? SessionId { get; } 34 | 35 | /// 36 | public string? CausationId { get; } 37 | } 38 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/CqrsMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Microsoft.Extensions.Logging; 3 | using CommunityToolkit.Diagnostics; 4 | using FluentResults; 5 | using Twilight.CQRS.Interfaces; 6 | 7 | namespace Twilight.CQRS; 8 | 9 | /// 10 | public abstract class CqrsMessageHandler : ICqrsMessageHandler 11 | where TMessage : class 12 | { 13 | private readonly IValidator? _validator; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The logger. 19 | /// The message validator. 20 | protected CqrsMessageHandler(ILogger logger, IValidator? validator = default) 21 | { 22 | _validator = validator; 23 | 24 | Logger = logger; 25 | } 26 | 27 | /// 28 | /// Gets the message handler logger. 29 | /// 30 | /// The logger. 31 | protected ILogger Logger { get; } 32 | 33 | /// 34 | public virtual async Task OnBeforeHandling(TMessage message, CancellationToken cancellationToken = default) 35 | => await Task.FromResult(Result.Try(() => 36 | { 37 | Guard.IsNotNull(message); 38 | })); 39 | 40 | /// 41 | public virtual async Task ValidateMessage(TMessage message, CancellationToken cancellationToken = default) 42 | { 43 | if (_validator == default) 44 | { 45 | return Result.Ok(); 46 | } 47 | 48 | var guardResult = Result.Try(() => 49 | { 50 | Guard.IsNotNull(message); 51 | }); 52 | 53 | if (guardResult.IsFailed) 54 | { 55 | return guardResult; 56 | } 57 | 58 | try 59 | { 60 | await _validator.ValidateAndThrowAsync(message, cancellationToken); 61 | 62 | return Result.Ok(); 63 | } 64 | catch (ValidationException ex) 65 | { 66 | return Result.Fail(ex.Message); 67 | } 68 | } 69 | 70 | /// 71 | public virtual async Task OnAfterHandling(TMessage message, CancellationToken cancellationToken = default) 72 | => await Task.FromResult(Result.Try(() => 73 | { 74 | Guard.IsNotNull(message); 75 | })); 76 | } 77 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Events/CqrsEvent.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Interfaces; 2 | 3 | namespace Twilight.CQRS.Events; 4 | 5 | /// 6 | /// 7 | /// Represents something that has already taken place in the domain. As such, always name an event with a 8 | /// past-participle verb, e.g. UserCreated. Events are facts and can be used to influence business decisions within 9 | /// the domain. Irrespective of whether an event has parameters or not, an event is always a 'fire-and-forget' 10 | /// operation and therefore does not return a response. 11 | /// 12 | /// Implements . 13 | /// Implements . 14 | /// 15 | /// 16 | /// 17 | public class CqrsEvent : CqrsMessage, ICqrsEvent 18 | { 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The event correlation identifier. 23 | /// The session identifier. 24 | /// 25 | /// The causation identifier. Identifies the message that caused this event to be produced. 26 | /// Optional. 27 | /// 28 | public CqrsEvent(string correlationId, string? sessionId = null, string? causationId = null) 29 | : base(correlationId, sessionId, causationId) 30 | { 31 | } 32 | } 33 | 34 | /// 35 | /// 36 | /// Represents something that has already taken place in the domain. As such, always name an event with a 37 | /// past-participle verb, e.g. UserCreated. Events are facts and can be used to influence business decisions within 38 | /// the domain. Irrespective of whether an event has parameters or not, an event is always a 'fire-and-forget' 39 | /// operation and therefore does not return a response. 40 | /// 41 | /// Implements . 42 | /// Implements . 43 | /// 44 | /// The type of the parameters. 45 | /// 46 | /// 47 | public class CqrsEvent : CqrsMessage, ICqrsEvent 48 | where TParameters : class 49 | { 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | /// The parameters. 54 | /// The event correlation identifier. 55 | /// The session identifier. 56 | /// 57 | /// The causation identifier. Identifies the message that caused this event to be produced. 58 | /// Optional. 59 | /// 60 | public CqrsEvent(TParameters parameters, string correlationId, string? sessionId = null, string? causationId = null) 61 | : base(correlationId, sessionId, causationId) 62 | => Params = parameters; 63 | 64 | /// 65 | /// Gets the typed event parameters. 66 | /// 67 | /// The parameters. 68 | public TParameters Params { get; } 69 | } 70 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Events/CqrsEventHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using CommunityToolkit.Diagnostics; 5 | using FluentResults; 6 | using Twilight.CQRS.Interfaces; 7 | // ReSharper disable ExplicitCallerInfoArgument as false positive for StartActivity 8 | 9 | namespace Twilight.CQRS.Events; 10 | 11 | /// 12 | /// 13 | /// Represents the ability to process (handle) events. An event handler receives a published event and 14 | /// brokers a result. A result is either a successful consumption of the event, or an exception. Events can be 15 | /// consumed by multiple event handlers. This class cannot be instantiated. 16 | /// 17 | /// Implements . 18 | /// Implements . 19 | /// 20 | /// The type of the event. 21 | /// The type of the event handler. 22 | /// 23 | /// 24 | public abstract class CqrsEventHandlerBase : CqrsMessageHandler, ICqrsEventHandler 25 | where TEvent : class, ICqrsEvent 26 | { 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The logger. 31 | /// The event validator. 32 | protected CqrsEventHandlerBase(ILogger logger, IValidator? validator = default) 33 | : base(logger, validator) 34 | { 35 | } 36 | 37 | /// 38 | public async Task Handle(TEvent @event, CancellationToken cancellationToken = default) 39 | { 40 | var guardResult = Result.Try(() => 41 | { 42 | Guard.IsNotNull(@event); 43 | }); 44 | 45 | if (guardResult.IsFailed) 46 | { 47 | return guardResult; 48 | } 49 | 50 | Result eventResult; 51 | 52 | using var activity = Activity.Current?.Source.StartActivity($"Handle {@event.GetType()}"); 53 | { 54 | using (var childSpan = Activity.Current?.Source.StartActivity("Pre event handling logic")) 55 | { 56 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase)}.{nameof(OnBeforeHandling)}")); 57 | 58 | var result = await OnBeforeHandling(@event, cancellationToken); 59 | 60 | if (!result.IsSuccess) 61 | { 62 | return result; 63 | } 64 | } 65 | 66 | using (var childSpan = Activity.Current?.Source.StartActivity("Validate event")) 67 | { 68 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase)}.{nameof(ValidateMessage)}")); 69 | 70 | var result = await ValidateMessage(@event, cancellationToken); 71 | 72 | if (!result.IsSuccess) 73 | { 74 | return result; 75 | } 76 | } 77 | 78 | using (var childSpan = Activity.Current?.Source.StartActivity("Handle event")) 79 | { 80 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase)}.{nameof(HandleEvent)}")); 81 | 82 | var result = await HandleEvent(@event, cancellationToken); 83 | 84 | if (!result.IsSuccess) 85 | { 86 | return result; 87 | } 88 | 89 | eventResult = result; 90 | } 91 | 92 | using (var childSpan = Activity.Current?.Source.StartActivity("Post event handling logic")) 93 | { 94 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase)}.{nameof(OnAfterHandling)}")); 95 | 96 | var result = await OnAfterHandling(@event, cancellationToken); 97 | 98 | if (!result.IsSuccess) 99 | { 100 | return result; 101 | } 102 | } 103 | } 104 | 105 | return eventResult; 106 | } 107 | 108 | /// 109 | /// Handles the event. 110 | /// 111 | /// The event. 112 | /// The cancellation token. 113 | /// A task that represents the asynchronous handle event operation. 114 | public abstract Task HandleEvent(TEvent @event, CancellationToken cancellationToken = default); 115 | } 116 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Queries/CqrsQuery.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Interfaces; 2 | 3 | namespace Twilight.CQRS.Queries; 4 | 5 | /// 6 | /// 7 | /// Represents a result and does not change the observable state of the system (i.e. is free of side effects). 8 | /// 9 | /// Implements . 10 | /// Implements . 11 | /// 12 | /// The type of the response. 13 | /// 14 | /// 15 | public class CqrsQuery : CqrsMessage, ICqrsQuery 16 | where TResponse : class 17 | { 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The query correlation identifier. 22 | /// The session identifier. 23 | /// 24 | /// The causation identifier. Identifies the message that caused this query to be produced. 25 | /// Optional. 26 | /// 27 | public CqrsQuery(string correlationId, string? sessionId = null, string? causationId = null) 28 | : base(correlationId, sessionId, causationId) 29 | { 30 | } 31 | } 32 | 33 | /// 34 | /// 35 | /// Represents a result and does not change the observable state of the system (i.e. is free of side effects). 36 | /// 37 | /// Implements . 38 | /// Implements . 39 | /// 40 | /// The type of the parameters. 41 | /// The type of the response. 42 | /// 43 | /// 44 | public class CqrsQuery : CqrsMessage, ICqrsQuery 45 | where TParameters : class 46 | where TResponse : class 47 | { 48 | /// 49 | /// Initializes a new instance of the class. 50 | /// 51 | /// The parameters. 52 | /// The query correlation identifier. 53 | /// The session identifier. 54 | /// 55 | /// The causation identifier. Identifies the message that caused this query to be produced. 56 | /// Optional. 57 | /// 58 | public CqrsQuery(TParameters parameters, string correlationId, string? sessionId = null, string? causationId = null) 59 | : base(correlationId, sessionId, causationId) 60 | => Params = parameters; 61 | 62 | /// 63 | /// Gets the typed query parameters. 64 | /// 65 | /// The parameters. 66 | public TParameters Params { get; } 67 | } 68 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Queries/CqrsQueryHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using CommunityToolkit.Diagnostics; 5 | using FluentResults; 6 | using Twilight.CQRS.Interfaces; 7 | // ReSharper disable ExplicitCallerInfoArgument as false positive for StartActivity 8 | 9 | namespace Twilight.CQRS.Queries; 10 | 11 | /// 12 | /// 13 | /// Represents the ability to process (handle) queries. A query handler receives a query and directs the query 14 | /// payload for processing. This class cannot be instantiated. 15 | /// 16 | /// Implements . 17 | /// Implements . 18 | /// 19 | /// The type of the query. 20 | /// The type of the query response. 21 | /// The type of the query handler. 22 | /// 23 | /// 24 | public abstract class CqrsQueryHandlerBase : CqrsMessageHandler, ICqrsQueryHandler 25 | where TQuery : class, ICqrsQuery 26 | { 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The logger. 31 | /// The query validator. 32 | protected CqrsQueryHandlerBase(ILogger logger, IValidator? validator = default) 33 | : base(logger, validator) 34 | { 35 | } 36 | 37 | /// 38 | public async Task> Handle(TQuery query, CancellationToken cancellationToken = default) 39 | { 40 | var guardResult = Result.Try(() => 41 | { 42 | Guard.IsNotNull(query); 43 | }); 44 | 45 | if (guardResult.IsFailed) 46 | { 47 | return guardResult; 48 | } 49 | 50 | Result queryResult; 51 | 52 | using var activity = Activity.Current?.Source.StartActivity($"Handle {query.GetType()}"); 53 | { 54 | using (var childSpan = Activity.Current?.Source.StartActivity("Pre query handling logic")) 55 | { 56 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase)}.{nameof(OnBeforeHandling)}")); 57 | 58 | var result = await OnBeforeHandling(query, cancellationToken); 59 | 60 | if (!result.IsSuccess) 61 | { 62 | return result; 63 | } 64 | } 65 | 66 | using (var childSpan = Activity.Current?.Source.StartActivity("Validate query")) 67 | { 68 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase)}.{nameof(ValidateMessage)}")); 69 | 70 | var result = await ValidateMessage(query, cancellationToken); 71 | 72 | if (!result.IsSuccess) 73 | { 74 | return result; 75 | } 76 | } 77 | 78 | using (var childSpan = Activity.Current?.Source.StartActivity("Handle query")) 79 | { 80 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase)}.{nameof(HandleQuery)}")); 81 | 82 | var result = await HandleQuery(query, cancellationToken); 83 | 84 | if (!result.IsSuccess) 85 | { 86 | return result; 87 | } 88 | 89 | queryResult = result; 90 | } 91 | 92 | using (var childSpan = Activity.Current?.Source.StartActivity("Post query handling logic")) 93 | { 94 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase)}.{nameof(OnAfterHandling)}")); 95 | 96 | var result = await OnAfterHandling(query, cancellationToken); 97 | 98 | if (!result.IsSuccess) 99 | { 100 | return result; 101 | } 102 | } 103 | } 104 | 105 | return queryResult; 106 | } 107 | 108 | /// 109 | /// Handles the query. 110 | /// 111 | /// The query. 112 | /// The cancellation token. 113 | /// 114 | /// A task that represents the asynchronous query handler operation. 115 | /// The task result contains the query execution response. 116 | /// 117 | protected abstract Task> HandleQuery(TQuery query, CancellationToken cancellationToken = default); 118 | } 119 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Queries/QueryResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Queries; 2 | 3 | /// 4 | /// Represents an encapsulated response from a query handler. 5 | /// Implements . 6 | /// 7 | /// The type of the payload. 8 | /// 9 | public class QueryResponse : CqrsMessage 10 | where TPayload : class 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The payload. 16 | /// The message correlation identifier. 17 | /// The session identifier. 18 | /// 19 | /// The causation identifier. Identifies the query that caused this response to be produced. 20 | /// Optional. 21 | /// 22 | public QueryResponse(TPayload payload, string correlationId, string? sessionId = null, string? causationId = null) 23 | : base(correlationId, sessionId, causationId) 24 | => Payload = payload; 25 | 26 | /// 27 | /// Gets the typed query response payload. 28 | /// 29 | /// The payload. 30 | public TPayload Payload { get; } 31 | } 32 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Twilight.CQRS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | Twilight.CQRS.xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Autofac.Tests.Unit/CqrsRegistrationExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using FluentAssertions; 3 | using Twilight.CQRS.Autofac.Tests.Unit.Setup; 4 | using Xunit; 5 | 6 | namespace Twilight.CQRS.Autofac.Tests.Unit; 7 | 8 | public sealed class CqrsRegistrationExtensionsTests 9 | { 10 | private readonly ContainerBuilder _builder = new(); 11 | 12 | // Setup 13 | 14 | [Fact] 15 | public void CallingRegisterForCqrsWithNullAssembliesDoesNotThrow() 16 | { 17 | // Arrange / Act 18 | var subjectResult = () => { _builder.RegisterAssemblyTypes(); }; 19 | 20 | // Assert 21 | subjectResult.Should().NotThrow(); 22 | } 23 | 24 | [Fact] 25 | public void RegisterForCqrsRegistersAssemblyServices() 26 | { 27 | // Arrange 28 | var assembly = typeof(TestCqrsCommandHandler).Assembly; 29 | 30 | _builder.RegisterAssemblyTypes(assembly); 31 | 32 | // Act 33 | var container = _builder.Build(); 34 | 35 | // Assert 36 | container.ComponentRegistry.Registrations.Count().Should().Be(4); 37 | container.ComponentRegistry.Registrations.Should().OnlyHaveUniqueItems(); 38 | 39 | var services = (from r in container.ComponentRegistry.Registrations 40 | from s in r.Services 41 | select s.Description).ToList(); 42 | 43 | var expectedServices = new List 44 | { 45 | typeof(TestCqrsCommandHandler).Namespace ?? string.Empty 46 | }; 47 | 48 | AssertOnExpectedServices(expectedServices, services); 49 | } 50 | 51 | // ReSharper disable once SuggestBaseTypeForParameter as don't want multiple enumeration 52 | private static void AssertOnExpectedServices(IEnumerable expectedServices, List services) 53 | { 54 | foreach (var expectedService in expectedServices) 55 | { 56 | var selectedService = (from service in services 57 | where service.Contains(expectedService) 58 | select service).FirstOrDefault(); 59 | 60 | selectedService.Should().NotBeNull(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Autofac.Tests.Unit/Setup/TestCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | using Twilight.CQRS.Tests.Common; 7 | 8 | namespace Twilight.CQRS.Autofac.Tests.Unit.Setup; 9 | 10 | internal sealed class TestCqrsCommandHandler( 11 | IMessageSender messageSender, 12 | ILogger logger, 13 | IValidator> validator) 14 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 15 | { 16 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 17 | => await Task.FromResult(Result.Ok()); 18 | } 19 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Autofac.Tests.Unit/Twilight.CQRS.Autofac.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/AutofacModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using Twilight.CQRS.Autofac; 5 | using Twilight.CQRS.Messaging.InMemory.Autofac; 6 | 7 | namespace Twilight.CQRS.Benchmarks; 8 | 9 | internal class AutofacModule : Module 10 | { 11 | protected override void Load(ContainerBuilder builder) 12 | { 13 | builder.RegisterCqrs(ThisAssembly); 14 | builder.AddAutofacInMemoryMessaging(); 15 | 16 | builder.RegisterGeneric(typeof(NullLogger<>)).As(typeof(ILogger<>)).SingleInstance(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Commands/SendCommandReceived.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Events; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Commands; 4 | 5 | internal sealed class SendCommandReceived(string correlationId, string? causationId = null) : CqrsEvent(correlationId, causationId); 6 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Commands/SendCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Commands; 3 | 4 | namespace Twilight.CQRS.Benchmarks.Commands; 5 | 6 | internal sealed class SendCommandValidator : AbstractValidator> 7 | { 8 | public SendCommandValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.CorrelationId).NotEmpty(); 12 | RuleFor(p => p.Params.Message).NotEmpty(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Commands/SendCqrsCommand.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Commands; 4 | 5 | internal sealed class SendCqrsCommand(MessageParameters parameters, string correlationId, string? causationId = null) : CqrsCommand(parameters, correlationId, causationId); 6 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Commands/SendCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | 7 | namespace Twilight.CQRS.Benchmarks.Commands; 8 | 9 | internal sealed class SendCqrsCommandHandler( 10 | IMessageSender messageSender, 11 | ILogger logger, 12 | IValidator>? validator = null) : CqrsCommandHandlerBase>(messageSender, logger, validator) 13 | { 14 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 15 | { 16 | var @event = new SendCommandReceived(command.CorrelationId, command.MessageId); 17 | 18 | await MessageSender.Publish(@event, cancellationToken); 19 | 20 | return Result.Ok(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Events/SendCqrsEvent.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Events; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Events; 4 | 5 | internal sealed class SendCqrsEvent(MessageParameters parameters, string correlationId, string? causationId = null) : CqrsEvent(parameters, correlationId, causationId); 6 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Events/SendCqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Events; 5 | 6 | namespace Twilight.CQRS.Benchmarks.Events; 7 | 8 | internal sealed class SendCqrsEventHandler( 9 | ILogger logger, 10 | IValidator>? validator = null) : CqrsEventHandlerBase>(logger, validator) 11 | { 12 | public override async Task HandleEvent(CqrsEvent cqrsEvent, CancellationToken cancellationToken = default) => await Task.FromResult(Result.Ok()); 13 | } 14 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Events/SendEventValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Events; 3 | 4 | namespace Twilight.CQRS.Benchmarks.Events; 5 | 6 | internal sealed class SendEventValidator : AbstractValidator> 7 | { 8 | public SendEventValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.CorrelationId).NotEmpty(); 12 | RuleFor(p => p.Params.Message).NotEmpty(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/InMemoryBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using BenchmarkDotNet.Attributes; 3 | using Twilight.CQRS.Benchmarks.Commands; 4 | using Twilight.CQRS.Benchmarks.Events; 5 | using Twilight.CQRS.Benchmarks.Queries; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | 8 | namespace Twilight.CQRS.Benchmarks; 9 | 10 | [MemoryDiagnoser] 11 | [KeepBenchmarkFiles] 12 | public class InMemoryBenchmarks 13 | { 14 | private readonly IMessageSender _messageSender; 15 | 16 | public InMemoryBenchmarks() 17 | { 18 | var builder = new ContainerBuilder(); 19 | 20 | builder.RegisterModule(); 21 | 22 | var container = builder.Build(); 23 | 24 | _messageSender = container.Resolve(); 25 | } 26 | 27 | [Benchmark(Description = nameof(SendCommand))] 28 | public void SendCommand() 29 | { 30 | for (var i = 0; i < 5000; i++) 31 | { 32 | var parameters = new MessageParameters("CqrsCommand"); 33 | var command = new SendCqrsCommand(parameters, Guid.NewGuid().ToString()); 34 | 35 | _messageSender.Send(command); 36 | } 37 | } 38 | 39 | [Benchmark(Description = nameof(SendQuery))] 40 | public void SendQuery() 41 | { 42 | for (var i = 0; i < 5000; i++) 43 | { 44 | var parameters = new MessageParameters("CqrsQuery"); 45 | var query = new SendCqrsQuery(parameters, Guid.NewGuid().ToString()); 46 | 47 | _ = _messageSender.Send(query); 48 | } 49 | } 50 | 51 | [Benchmark(Description = nameof(PublishEvent))] 52 | public void PublishEvent() 53 | { 54 | for (var i = 0; i < 10000; i++) 55 | { 56 | var parameters = new MessageParameters("CqrsEvent"); 57 | var @event = new SendCqrsEvent(parameters, Guid.NewGuid().ToString()); 58 | 59 | _messageSender.Publish(@event); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/MessageParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Benchmarks; 2 | 3 | internal sealed record MessageParameters(string Message); 4 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using Twilight.CQRS.Benchmarks; 3 | 4 | // Run in Release Build Configuration 5 | var summary = BenchmarkRunner.Run(); 6 | 7 | Console.WriteLine(summary); 8 | Console.ReadLine(); 9 | 10 | Environment.Exit(0); 11 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Queries/QueryResponsePayload.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Benchmarks.Queries; 2 | 3 | internal sealed record QueryResponsePayload(string Response); 4 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Queries/SendCqrsQuery.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Queries; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Queries; 4 | 5 | internal sealed class SendCqrsQuery(MessageParameters parameters, string correlationId, string? causationId = null) : CqrsQuery>(parameters, correlationId, causationId); 6 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Queries/SendCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Queries; 5 | 6 | namespace Twilight.CQRS.Benchmarks.Queries; 7 | 8 | internal sealed class SendCqrsQueryHandler( 9 | ILogger logger, 10 | IValidator>>? validator = null) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 11 | { 12 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 13 | { 14 | var payload = new QueryResponsePayload("CqrsQuery Response"); 15 | var response = new QueryResponse(payload, query.CorrelationId, query.MessageId); 16 | 17 | return await Task.FromResult(Result.Ok(response)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Queries/SendQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Events; 3 | 4 | namespace Twilight.CQRS.Benchmarks.Queries; 5 | 6 | internal sealed class SendQueryValidator : AbstractValidator> 7 | { 8 | public SendQueryValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.CorrelationId).NotEmpty(); 12 | RuleFor(p => p.Params.Message).NotEmpty(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Twilight.CQRS.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | 12.0 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/AutofacDependencyResolutionFailureTests.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using FluentAssertions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Twilight.CQRS.Commands; 6 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Parameters; 7 | using Twilight.CQRS.Messaging.Interfaces; 8 | using Twilight.CQRS.Tests.Common; 9 | using Xunit; 10 | 11 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration; 12 | 13 | public sealed class AutofacDependencyResolutionFailureTests : IAsyncLifetime 14 | { 15 | private static IContainer? _container; 16 | 17 | private readonly IMessageSender _subject; 18 | 19 | public AutofacDependencyResolutionFailureTests() 20 | { 21 | var builder = new ContainerBuilder(); 22 | 23 | builder.RegisterType().As(); 24 | builder.RegisterType>().As>(); 25 | 26 | _container = builder.Build(); 27 | 28 | _subject = _container.Resolve(); 29 | } 30 | 31 | public async Task InitializeAsync() 32 | => await Task.CompletedTask; 33 | 34 | public async Task DisposeAsync() 35 | { 36 | _container?.Dispose(); 37 | 38 | await Task.CompletedTask; 39 | } 40 | 41 | [Fact] 42 | public async Task MessageSenderThrowsWhenDependencyResolutionFails() 43 | { 44 | // Arrange 45 | var parameters = new MultipleHandlersParameters(); 46 | var command = new CqrsCommand(parameters, Constants.CorrelationId); 47 | 48 | // Act 49 | var result = await _subject.Send(command, CancellationToken.None); 50 | 51 | // Assert 52 | result.IsSuccess.Should().Be(false); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/AutofacInMemoryMessageSenderFailureTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Events; 4 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 5 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Parameters; 6 | using Twilight.CQRS.Tests.Common; 7 | using Xunit; 8 | 9 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration; 10 | 11 | public sealed class AutofacInMemoryMessageSenderFailureTests : IntegrationTestBase 12 | { 13 | [Fact] 14 | public async Task MessageSenderThrowsWhenCommandHandlerDoesNotExist() 15 | { 16 | // Arrange 17 | var command = new CqrsCommand(Constants.CorrelationId); 18 | 19 | // Act 20 | var result = await Subject.Send(command, CancellationToken.None); 21 | 22 | // Assert 23 | result.IsSuccess.Should().BeFalse(); 24 | result.Errors[0].Message.Should().Be("No handler cold be found for this request."); 25 | } 26 | 27 | [Fact] 28 | public async Task MessageSenderThrowsWhenMultipleCommandHandlersResolved() 29 | { 30 | // Arrange 31 | var parameters = new MultipleHandlersParameters(); 32 | var command = new CqrsCommand(parameters, Constants.CorrelationId); 33 | 34 | // Act 35 | var result = await Subject.Send(command, CancellationToken.None); 36 | 37 | // Assert 38 | result.IsSuccess.Should().BeFalse(); 39 | result.Errors[0].Message.Should().Be("Multiple handlers found. A command may only have one handler."); 40 | } 41 | 42 | [Fact] 43 | public async Task MessageSenderThrowsWhenNoHandlerRegisteredForEvent() 44 | { 45 | // Arrange 46 | var @event = new CqrsEvent(string.Empty, Constants.CorrelationId, Constants.CausationId); 47 | 48 | // Act 49 | var result = await Subject.Publish(@event, CancellationToken.None); 50 | 51 | // Assert 52 | result.IsSuccess.Should().BeFalse(); 53 | result.Errors[0].Message.Should().Be("No handler cold be found for this request."); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/AutofacInMemoryMessageSenderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NSubstitute; 3 | using Twilight.CQRS.Commands; 4 | using Twilight.CQRS.Events; 5 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 6 | using Twilight.CQRS.Queries; 7 | using Twilight.CQRS.Tests.Common; 8 | using Xunit; 9 | 10 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration; 11 | 12 | public sealed class AutofacInMemoryMessageSenderTests : IntegrationTestBase 13 | { 14 | [Fact] 15 | public async Task MessageSenderCallsCorrectHandlerForCommand() 16 | { 17 | // Arrange 18 | var command = new CqrsCommand(new TestParameters(nameof(MessageSenderCallsCorrectHandlerForCommand)), Constants.CorrelationId); 19 | 20 | // Act 21 | await Subject.Send(command, CancellationToken.None); 22 | 23 | // Assert 24 | await Verifier.Received(1).Receive(Arg.Is(command.Params.Value)); 25 | } 26 | 27 | [Fact] 28 | public async Task MessageSenderCallsCorrectHandlerForCommandWithResponse() 29 | { 30 | // Arrange 31 | var command = new CqrsCommand>(new TestParameters(nameof(MessageSenderCallsCorrectHandlerForCommandWithResponse)), Constants.CorrelationId); 32 | 33 | // Act 34 | var result = await Subject.Send(command, CancellationToken.None); 35 | 36 | // Assert 37 | await Verifier.Received(1).Receive(Arg.Is(command.Params.Value)); 38 | 39 | result.Value.Should().NotBeNull(); 40 | result.Value.CorrelationId.Should().Be(Constants.CorrelationId); 41 | } 42 | 43 | [Fact] 44 | public async Task MessageSenderCallsCorrectHandlerForEvent() 45 | { 46 | // Arrange 47 | var @event = new CqrsEvent(new TestParameters(nameof(MessageSenderCallsCorrectHandlerForEvent)), Constants.CorrelationId, Constants.CausationId); 48 | 49 | // Act 50 | await Subject.Publish(@event, CancellationToken.None); 51 | 52 | // Assert 53 | await Verifier.Received(1).Receive(Arg.Is(@event.Params.Value)); 54 | } 55 | 56 | [Fact] 57 | public async Task MessageSenderCallsCorrectHandlerForEvents() 58 | { 59 | // Arrange 60 | var @event = new CqrsEvent(new TestParameters(nameof(MessageSenderCallsCorrectHandlerForEvents)), Constants.CorrelationId, Constants.CausationId); 61 | var events = new List> 62 | { 63 | @event 64 | }; 65 | 66 | var enumerableEvents = events.AsEnumerable(); 67 | 68 | // Act 69 | await Subject.Publish(enumerableEvents, CancellationToken.None); 70 | 71 | // Assert 72 | await Verifier.Received(1).Receive(@event.Params.Value); 73 | } 74 | 75 | [Fact] 76 | public async Task MessageSenderCallsCorrectHandlerForQuery() 77 | { 78 | // Arrange 79 | var query = new CqrsQuery>(new TestParameters(nameof(MessageSenderCallsCorrectHandlerForQuery)), Constants.CorrelationId); 80 | 81 | // Act 82 | var result = await Subject.Send(query, CancellationToken.None); 83 | 84 | // Assert 85 | await Verifier.Received(1).Receive(Arg.Is(query.Params.Value)); 86 | 87 | result.Value.Should().NotBeNull(); 88 | result.Value.CorrelationId.Should().Be(Constants.CorrelationId); 89 | } 90 | 91 | [Fact] 92 | public async Task MessageSenderThrowsWhenCommandHandlerIsNotFound() 93 | { 94 | // Arrange 95 | var command = new CqrsCommand(string.Empty, Constants.CorrelationId, Constants.CausationId); 96 | 97 | // Act 98 | var result = await Subject.Send(command, CancellationToken.None); 99 | 100 | // Assert 101 | result.IsSuccess.Should().BeFalse(); 102 | result.Errors[0].Message.Should().Be("No handler cold be found for this request."); 103 | } 104 | 105 | [Fact] 106 | public async Task MessageSenderThrowsWhenEventHandlerIsNotFound() 107 | { 108 | // Arrange 109 | var @event = new CqrsEvent(string.Empty, Constants.CorrelationId, Constants.CausationId); 110 | 111 | // Act 112 | var result = await Subject.Publish(@event, CancellationToken.None); 113 | 114 | // Assert 115 | result.IsSuccess.Should().BeFalse(); 116 | result.Errors[0].Message.Should().Be("No handler cold be found for this request."); 117 | } 118 | 119 | [Fact] 120 | public async Task MessageSenderThrowsWhenQueryHandlerIsNotFound() 121 | { 122 | // Arrange 123 | var query = new CqrsQuery>(string.Empty, Constants.CorrelationId); 124 | 125 | // Act 126 | var result = await Subject.Send(query, CancellationToken.None); 127 | 128 | // Assert 129 | result.IsSuccess.Should().BeFalse(); 130 | result.Errors[0].Message.Should().Be("No handler cold be found for this request."); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/MultipleHandlersOneHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Parameters; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | 8 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 9 | 10 | internal sealed class MultipleHandlersOneHandler( 11 | IMessageSender messageSender, 12 | ILogger logger, 13 | IValidator> validator) 14 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 15 | { 16 | public override Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 17 | => throw new NotImplementedException(); 18 | } 19 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/MultipleHandlersTwoHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Parameters; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | 8 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 9 | 10 | internal sealed class MultipleHandlersTwoHandler( 11 | IMessageSender messageSender, 12 | ILogger logger, 13 | IValidator> validator) 14 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 15 | { 16 | public override Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 17 | => throw new NotImplementedException(); 18 | } 19 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | using Twilight.CQRS.Tests.Common; 7 | 8 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 9 | 10 | internal sealed class TestCqrsCommandHandler(ITestService service, 11 | IMessageSender messageSender, 12 | ILogger logger, 13 | IValidator> validator) 14 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 15 | { 16 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 17 | => await service.Receive(command.Params.Value); 18 | } 19 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsCommandWithResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | using Twilight.CQRS.Tests.Common; 7 | 8 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 9 | 10 | internal sealed class TestCqrsCommandWithResponseHandler(IMessageSender messageSender, 11 | ITestService service, 12 | ILogger logger, 13 | IValidator>> validator) 14 | : CqrsCommandHandlerBase>, CqrsCommandResponse>(messageSender, logger, validator) 15 | { 16 | protected override async Task>> HandleCommand(CqrsCommand> command, CancellationToken cancellationToken = default) 17 | { 18 | await service.Receive(command.Params.Value); 19 | 20 | var response = new CqrsCommandResponse(nameof(TestCqrsCommandWithResponseHandler), command.CorrelationId, command.MessageId); 21 | 22 | return response; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Events; 5 | using Twilight.CQRS.Tests.Common; 6 | 7 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 8 | 9 | internal sealed class TestCqrsEventHandler(ITestService service, 10 | ILogger logger, 11 | IValidator> validator) : CqrsEventHandlerBase>(logger, validator) 12 | { 13 | public override async Task HandleEvent(CqrsEvent cqrsEvent, CancellationToken cancellationToken = default) 14 | => await service.Receive(cqrsEvent.Params.Value); 15 | } 16 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Queries; 5 | using Twilight.CQRS.Tests.Common; 6 | 7 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 8 | 9 | internal sealed class TestCqrsQueryHandler(ITestService service, 10 | ILogger logger, 11 | IValidator>> validator) 12 | : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 13 | { 14 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 15 | { 16 | await service.Receive(query.Params.Value); 17 | 18 | var response = new QueryResponse(nameof(TestCqrsQueryHandler), query.CorrelationId, query.MessageId); 19 | 20 | return Result.Ok(response); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/ITestService.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 4 | 5 | public interface ITestService 6 | { 7 | Task Receive(string parameters); 8 | } 9 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/IVerifier.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 4 | 5 | public interface IVerifier 6 | { 7 | Task Receive(string parameter); 8 | } 9 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using FluentAssertions; 3 | using FluentResults; 4 | using NSubstitute; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | using Xunit; 7 | 8 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 9 | 10 | public abstract class IntegrationTestBase : IAsyncLifetime 11 | { 12 | private readonly IContainer _container; 13 | 14 | protected IntegrationTestBase() 15 | { 16 | Verifier = Substitute.For(); 17 | 18 | Verifier.Receive(Arg.Any()).Returns(Result.Ok()); 19 | 20 | var builder = new ContainerBuilder(); 21 | var testService = new TestService(Verifier).As(); 22 | 23 | builder.RegisterModule(); 24 | builder.RegisterInstance(testService); 25 | 26 | _container = builder.Build(); 27 | 28 | Subject = _container.Resolve(); 29 | } 30 | 31 | protected IMessageSender Subject { get; } 32 | 33 | protected IVerifier Verifier { get; } 34 | 35 | public async Task InitializeAsync() 36 | => await Task.CompletedTask; 37 | 38 | public async Task DisposeAsync() 39 | { 40 | _container.Dispose(); 41 | 42 | await Task.CompletedTask; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/IntegrationTestModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using Twilight.CQRS.Autofac; 5 | 6 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 7 | 8 | internal sealed class IntegrationTestModule : Module 9 | { 10 | protected override void Load(ContainerBuilder builder) 11 | { 12 | builder.RegisterCqrs([ThisAssembly]); 13 | builder.AddAutofacInMemoryMessaging(); 14 | builder.RegisterGeneric(typeof(NullLogger<>)).As(typeof(ILogger<>)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Parameters/MultipleHandlersParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Parameters; 2 | 3 | internal sealed record MultipleHandlersParameters; 4 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/TestService.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | 3 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 4 | 5 | internal sealed class TestService(IVerifier verifier) : ITestService 6 | { 7 | public async Task Receive(string parameters) 8 | => await verifier.Receive(parameters); 9 | } 10 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Validators/MultipleHandlersValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Parameters; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Validators; 6 | 7 | internal sealed class MultipleHandlersValidator : AbstractValidator> 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Validators/TestCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Validators; 6 | 7 | internal sealed class TestCommandValidator : AbstractValidator> 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Validators/TestCommandWithResponseValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Validators; 6 | 7 | internal sealed class TestCommandWithResponseValidator : AbstractValidator>> 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Validators/TestEventValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Events; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Validators; 6 | 7 | internal sealed class TestEventValidator : AbstractValidator> 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Validators/TestQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Queries; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Validators; 6 | 7 | internal sealed class TestQueryValidator : AbstractValidator>> 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit/AutofacInMemoryMessagingRegistrationExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using FluentAssertions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | using Xunit; 7 | 8 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit; 9 | 10 | public sealed class AutofacInMemoryMessagingRegistrationExtensionsTests 11 | { 12 | [Fact] 13 | public void AddInMemoryMessagingShouldRegisterInMemoryMessageSender() 14 | { 15 | // Arrange 16 | var builder = new ContainerBuilder(); 17 | 18 | builder.AddAutofacInMemoryMessaging(); 19 | builder.RegisterType>().As>(); 20 | 21 | // Act 22 | var container = builder.Build(); 23 | 24 | // Assert 25 | container.ComponentRegistry.Registrations.Count().Should().Be(3); 26 | 27 | container.ComponentRegistry 28 | .Registrations.Any(x => x.Services.Any(s => s.Description == typeof(IMessageSender).FullName)) 29 | .Should().BeTrue(); 30 | 31 | var messageSender = container.Resolve(); 32 | 33 | messageSender.Should().BeAssignableTo(); 34 | 35 | container.Dispose(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Common/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Tests.Common; 2 | 3 | public static class Constants 4 | { 5 | public static string CorrelationId => Guid.Parse("02F9310E-13B1-4DFD-9ACF-7D55DA65D071").ToString(); 6 | 7 | public static string CausationId => Guid.Parse("02F9310E-13B1-4DFD-9ACF-7D55DA65D072").ToString(); 8 | 9 | public static string SessionId => Guid.Parse("D27058A4-EB68-4852-9EC3-5963446CF975").ToString(); 10 | } 11 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Common/NonValidatingTestParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Tests.Common; 2 | 3 | public record NonValidatingTestParameters(string Value); 4 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Common/TestParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Tests.Common; 2 | 3 | public sealed class TestParameters(string value) 4 | { 5 | public TestParameters() 6 | : this("test") 7 | { 8 | } 9 | 10 | public string Value { get; } = value; 11 | } 12 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Common/Twilight.CQRS.Tests.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Commands/CommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Microsoft.Extensions.Logging; 3 | using NSubstitute; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Events; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | using Twilight.CQRS.Tests.Common; 8 | using Xunit; 9 | 10 | namespace Twilight.CQRS.Tests.Unit.Commands; 11 | 12 | public sealed class CommandHandlerTests 13 | { 14 | private readonly IMessageSender _messageSender; 15 | private readonly TestCqrsCommandHandler _subject; 16 | 17 | public CommandHandlerTests() 18 | { 19 | // Setup 20 | _messageSender = Substitute.For(); 21 | 22 | var logger = Substitute.For>(); 23 | 24 | IValidator> validator = new TestParametersValidator(); 25 | 26 | _subject = new TestCqrsCommandHandler(_messageSender, logger, validator); 27 | } 28 | 29 | [Fact] 30 | public async Task HandlerShouldPublishEventWhenHandling() 31 | { 32 | // Arrange 33 | var testCommand = new CqrsCommand(new TestParameters(), Constants.CorrelationId); 34 | 35 | // Act 36 | await _subject.HandleCommand(testCommand, CancellationToken.None); 37 | 38 | // Assert 39 | await _messageSender.Received(1).Publish(Arg.Any>(), Arg.Is(CancellationToken.None)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Commands/CommandTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Tests.Common; 4 | using Xunit; 5 | 6 | namespace Twilight.CQRS.Tests.Unit.Commands; 7 | 8 | public sealed class CommandTests 9 | { 10 | private readonly TestParameters _params = new(); 11 | 12 | // Setup 13 | 14 | [Fact] 15 | public void CommandWithoutParametersShouldAssignCausationId() 16 | { 17 | // arrange / Act 18 | var subject = new CqrsCommand(Constants.CorrelationId, Constants.CausationId); 19 | 20 | // Assert 21 | subject.CausationId.Should().Be(Constants.CausationId); 22 | } 23 | 24 | [Fact] 25 | public void CommandWithoutParametersShouldAssignCorrelationId() 26 | { 27 | // Arrange / Act 28 | var subject = new CqrsCommand(Constants.CorrelationId, Constants.CausationId); 29 | 30 | //Assert 31 | subject.CorrelationId.Should().Be(Constants.CorrelationId); 32 | } 33 | 34 | [Fact] 35 | public void CommandWithParametersShouldAssignCausationId() 36 | { 37 | // Arrange / Act 38 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 39 | 40 | // Assert 41 | subject.CausationId.Should().Be(Constants.CausationId); 42 | } 43 | 44 | [Fact] 45 | public void CommandWithParametersShouldAssignCorrelationId() 46 | { 47 | // Arrange / Act 48 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 49 | 50 | // Assert 51 | subject.CorrelationId.Should().Be(Constants.CorrelationId); 52 | } 53 | 54 | [Fact] 55 | public void CommandWithParametersShouldAssignMessageId() 56 | { 57 | // Arrange / Act 58 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 59 | 60 | subject.MessageId.Should().NotBeEmpty(); 61 | } 62 | 63 | [Fact] 64 | public void CommandWithParametersShouldAssignParameters() 65 | { 66 | // Arrange / Act 67 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 68 | 69 | // Assert 70 | subject.Params.Should().NotBeNull(); 71 | subject.Params.Should().BeEquivalentTo(_params); 72 | } 73 | 74 | [Fact] 75 | public void CommandWithParametersAssignsSessionId() 76 | { 77 | // Arrange / Act 78 | var subject = new CqrsCommand(_params, Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 79 | 80 | //Assert 81 | subject.SessionId.Should().Be(Constants.SessionId); 82 | } 83 | [Fact] 84 | public void CommandWithoutParametersAssignsSessionId() 85 | { 86 | // Arrange / Act 87 | var subject = new CqrsCommand(Constants.CorrelationId, Constants.CausationId, Constants.SessionId); 88 | 89 | //Assert 90 | subject.SessionId.Should().Be(Constants.SessionId); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Commands/TestCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Events; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | using Twilight.CQRS.Tests.Common; 8 | 9 | namespace Twilight.CQRS.Tests.Unit.Commands; 10 | 11 | public sealed class TestCqrsCommandHandler( 12 | IMessageSender messageSender, 13 | ILogger logger, 14 | IValidator> validator) 15 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 16 | { 17 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 18 | { 19 | await MessageSender.Publish(new CqrsEvent(command.Params, command.CorrelationId, command.MessageId), cancellationToken); 20 | 21 | return await Task.FromResult(Result.Ok()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Events/EventTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Twilight.CQRS.Events; 3 | using Twilight.CQRS.Tests.Common; 4 | using Xunit; 5 | 6 | namespace Twilight.CQRS.Tests.Unit.Events; 7 | 8 | public sealed class EventTests 9 | { 10 | private readonly TestParameters _params = new(); 11 | 12 | // Setup 13 | 14 | [Fact] 15 | public void EventWithoutParametersShouldAssignCausationId() 16 | { 17 | // Arrange / Act 18 | var subject = new CqrsEvent(Constants.CorrelationId, null, Constants.CausationId); 19 | 20 | // Assert 21 | subject.CausationId.Should().Be(Constants.CausationId); 22 | } 23 | 24 | [Fact] 25 | public void EventWithoutParametersShouldAssignCorrelationId() 26 | { 27 | // Arrange / Act 28 | var subject = new CqrsEvent(Constants.CorrelationId, null, Constants.CausationId); 29 | 30 | // Assert 31 | subject.CorrelationId.Should().Be(Constants.CorrelationId); 32 | } 33 | 34 | [Fact] 35 | public void EventWithParametersShouldAssignCausationId() 36 | { 37 | // Arrange / Act 38 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 39 | 40 | // Assert 41 | subject.CausationId.Should().Be(Constants.CausationId); 42 | } 43 | 44 | [Fact] 45 | public void EventWithParametersShouldAssignCorrelationId() 46 | { 47 | // Arrange / Act 48 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 49 | 50 | // Assert 51 | subject.CorrelationId.Should().Be(Constants.CorrelationId); 52 | } 53 | 54 | [Fact] 55 | public void EventWithParametersShouldAssignMessageId() 56 | { 57 | // Arrange / Act 58 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 59 | 60 | // Assert 61 | subject.MessageId.Should().NotBeEmpty(); 62 | } 63 | 64 | [Fact] 65 | public void EventWithParametersShouldAssignParameters() 66 | { 67 | // Arrange / Act 68 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 69 | 70 | // Assert 71 | subject.Params.Should().NotBeNull(); 72 | subject.Params.Should().BeEquivalentTo(_params); 73 | } 74 | 75 | [Fact] 76 | public void QueryWithParametersAssignsSessionId() 77 | { 78 | // Arrange / Act 79 | var subject = new CqrsEvent(_params, Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 80 | 81 | //Assert 82 | subject.SessionId.Should().Be(Constants.SessionId); 83 | } 84 | 85 | [Fact] 86 | public void QueryWithoutParametersAssignsSessionId() 87 | { 88 | // Arrange / Act 89 | var subject = new CqrsEvent(Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 90 | 91 | //Assert 92 | subject.SessionId.Should().Be(Constants.SessionId); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/NonValidatingTestCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Queries; 5 | 6 | namespace Twilight.CQRS.Tests.Unit.Queries; 7 | 8 | internal sealed class NonValidatingTestCqrsQueryHandler( 9 | ILogger logger, 10 | IValidator>>? validator = default) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 11 | { 12 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 13 | { 14 | var response = new QueryResponse(string.Empty, query.CorrelationId, query.MessageId); 15 | 16 | return await Task.FromResult(Result.Ok(response)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/QueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using NSubstitute; 5 | using Twilight.CQRS.Queries; 6 | using Twilight.CQRS.Tests.Common; 7 | using Xunit; 8 | 9 | namespace Twilight.CQRS.Tests.Unit.Queries; 10 | 11 | public sealed class QueryHandlerTests 12 | { 13 | private readonly TestCqrsQueryHandler _subject; 14 | 15 | // Setup 16 | public QueryHandlerTests() 17 | { 18 | var logger = Substitute.For>(); 19 | 20 | IValidator>> validator = new TestQueryParametersValidator(); 21 | 22 | _subject = new TestCqrsQueryHandler(logger, validator); 23 | } 24 | 25 | [Fact] 26 | public async Task HandlerShouldHandleQuery() 27 | { 28 | // Arrange 29 | var testQuery = new CqrsQuery>(new TestParameters(), Constants.CorrelationId); 30 | 31 | // Act 32 | var response = await _subject.Handle(testQuery, CancellationToken.None); 33 | 34 | // Assert 35 | response.Value.Payload.Value.Should().Be("1"); 36 | } 37 | 38 | [Fact] 39 | public async Task HandlerShouldNotThrowWhenValidatingValidQueryParameters() 40 | { 41 | // Arrange 42 | var testQuery = new CqrsQuery>(new TestParameters(), Constants.CorrelationId); 43 | 44 | // Act 45 | var subjectResult = async () => { await _subject.Handle(testQuery, CancellationToken.None); }; 46 | 47 | // Assert 48 | await subjectResult.Should().NotThrowAsync(); 49 | } 50 | 51 | [Fact] 52 | public async Task HandlerShouldThrowWhenValidatingInvalidQueryParameters() 53 | { 54 | // Arrange 55 | var testQuery = new CqrsQuery>(new TestParameters(string.Empty), Constants.CorrelationId); 56 | 57 | // Act 58 | var result = await _subject.Handle(testQuery, CancellationToken.None); 59 | 60 | // Assert 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/QueryTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Twilight.CQRS.Queries; 3 | using Twilight.CQRS.Tests.Common; 4 | using Xunit; 5 | 6 | namespace Twilight.CQRS.Tests.Unit.Queries; 7 | 8 | public sealed class QueryTests 9 | { 10 | private readonly TestParameters _params = new(); 11 | 12 | [Fact] 13 | public void QueryWithoutParametersShouldAssignCausationId() 14 | { 15 | // Arrange / Act 16 | var subject = new CqrsQuery(Constants.CorrelationId, null, Constants.CausationId); 17 | 18 | // Assert 19 | subject.CausationId.Should().Be(Constants.CausationId); 20 | } 21 | 22 | [Fact] 23 | public void QueryWithoutParametersShouldAssignCorrelationId() 24 | { 25 | // Arrange / Act 26 | var subject = new CqrsQuery(Constants.CorrelationId, Constants.CausationId); 27 | 28 | // Assert 29 | subject.CorrelationId.Should().Be(Constants.CorrelationId); 30 | } 31 | 32 | [Fact] 33 | public void QueryWithParametersShouldAssignCausationId() 34 | { 35 | // Arrange / Act 36 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 37 | 38 | // Assert 39 | subject.CausationId.Should().Be(Constants.CausationId); 40 | } 41 | 42 | [Fact] 43 | public void QueryWithParametersShouldAssignCorrelationId() 44 | { 45 | // Arrange / Act 46 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 47 | 48 | // Assert 49 | subject.CorrelationId.Should().Be(Constants.CorrelationId); 50 | } 51 | 52 | [Fact] 53 | public void QueryWithParametersShouldAssignMessageId() 54 | { 55 | // Arrange / Act 56 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 57 | 58 | // Assert 59 | subject.MessageId.Should().NotBeEmpty(); 60 | } 61 | 62 | [Fact] 63 | public void QueryWithParametersShouldAssignParameters() 64 | { 65 | // Arrange / Act 66 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 67 | 68 | // Assert 69 | subject.Params.Should().NotBeNull(); 70 | subject.Params.Should().BeEquivalentTo(_params); 71 | } 72 | 73 | [Fact] 74 | public void QueryWithParametersShouldAssignSessionId() 75 | { 76 | // Arrange / Act 77 | var subject = new CqrsQuery(_params, Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 78 | 79 | // Assert 80 | subject.SessionId.Should().Be(Constants.SessionId); 81 | } 82 | 83 | [Fact] 84 | public void QueryWithoutParametersShouldAssignSessionId() 85 | { 86 | // Arrange / Act 87 | var subject = new CqrsQuery(Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 88 | 89 | // Assert 90 | subject.SessionId.Should().Be(Constants.SessionId); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/TestCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using FluentValidation; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Queries; 5 | using Twilight.CQRS.Tests.Common; 6 | 7 | namespace Twilight.CQRS.Tests.Unit.Queries; 8 | 9 | public sealed class TestCqrsQueryHandler( 10 | ILogger logger, 11 | IValidator>> validator) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 12 | { 13 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 14 | { 15 | var payload = new TestQueryResponse 16 | { 17 | Value = "1" 18 | }; 19 | 20 | var response = new QueryResponse(payload, query.CorrelationId, query.MessageId); 21 | 22 | return await Task.FromResult(Result.Ok(response)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/TestQueryParametersValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Queries; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Tests.Unit.Queries; 6 | 7 | internal sealed class TestQueryParametersValidator : AbstractValidator>> 8 | { 9 | public TestQueryParametersValidator() 10 | => RuleFor(p => p.Params.Value).NotEmpty(); 11 | } 12 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/TestQueryResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Tests.Unit.Queries; 2 | 3 | public sealed class TestQueryResponse 4 | { 5 | public string Value { get; init; } = string.Empty; 6 | } 7 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/TestParametersValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Tests.Unit; 6 | 7 | internal sealed class TestParametersValidator : AbstractValidator> 8 | { 9 | public TestParametersValidator() 10 | => RuleFor(p => p.Params.Value).NotEmpty(); 11 | } 12 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Twilight.CQRS.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 12.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Twilight.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7F80EF36-698E-4D2C-9F28-86684EC5DF4A}" 7 | ProjectSection(SolutionItems) = preProject 8 | DependenciesGraph.png = DependenciesGraph.png 9 | LICENSE = LICENSE 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Interfaces", "Src\Twilight.CQRS.Interfaces\Twilight.CQRS.Interfaces.csproj", "{FBF4A6DF-A683-4960-AF89-87079CBAAEC5}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Messaging.Interfaces", "Src\Twilight.CQRS.Messaging.Interfaces\Twilight.CQRS.Messaging.Interfaces.csproj", "{DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS", "Src\Twilight.CQRS\Twilight.CQRS.csproj", "{E7F1DF13-5089-41CE-8D5E-D461E99CB426}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Autofac", "Src\Twilight.CQRS.Autofac\Twilight.CQRS.Autofac.csproj", "{8C6EDB84-1065-4643-BAFB-74E4992452B1}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Messaging.InMemory.Autofac", "Src\Twilight.CQRS.Messaging.InMemory.Autofac\Twilight.CQRS.Messaging.InMemory.Autofac.csproj", "{97232A73-765D-4E57-B8F7-178C5FD1F906}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration", "Test\Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration\Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.csproj", "{143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit", "Test\Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit\Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit.csproj", "{5E69A57A-B4BF-4C25-82AD-A23CE049C119}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Tests.Common", "Test\Twilight.CQRS.Tests.Common\Twilight.CQRS.Tests.Common.csproj", "{EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Tests.Unit", "Test\Twilight.CQRS.Tests.Unit\Twilight.CQRS.Tests.Unit.csproj", "{01860F80-5AD9-4251-B0F4-6DE9507DA39F}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Autofac.Tests.Unit", "Test\Twilight.CQRS.Autofac.Tests.Unit\Twilight.CQRS.Autofac.Tests.Unit.csproj", "{18337CF2-C3EA-4201-A597-DC9649D4EB2A}" 32 | EndProject 33 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CQRS", "CQRS", "{8C3A64F8-54D3-4EFA-B910-2DDE79D7206C}" 34 | EndProject 35 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8B189B56-AD4C-4617-AC32-999567271C58}" 36 | EndProject 37 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{3C99F06C-4AD2-4328-B8C5-6988BF03F24A}" 38 | EndProject 39 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.Samples.CQRS", "Samples\Twilight.Samples.CQRS\Twilight.Samples.CQRS.csproj", "{3309A085-5888-4015-8382-4A7F1B493BDB}" 40 | EndProject 41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.Samples.Common", "Samples\Twilight.Samples.Common\Twilight.Samples.Common.csproj", "{15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}" 42 | EndProject 43 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilight.CQRS.Benchmarks", "Test\Twilight.CQRS.Benchmarks\Twilight.CQRS.Benchmarks.csproj", "{D64C370E-4942-4A93-9F94-A142B38830DB}" 44 | EndProject 45 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unit", "Unit", "{8D6B35D2-B87B-4A92-A42B-11C6B0612726}" 46 | EndProject 47 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Integration", "Integration", "{6C54A309-86AB-4BCC-ACEE-6C400EEF180F}" 48 | EndProject 49 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Performance", "Performance", "{0E56099D-FEF0-4FBB-93E3-36B3F641DE42}" 50 | EndProject 51 | Global 52 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 53 | Debug|Any CPU = Debug|Any CPU 54 | Release|Any CPU = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 57 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 82 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Debug|Any CPU.Build.0 = Debug|Any CPU 83 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Release|Any CPU.Build.0 = Release|Any CPU 85 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 86 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Debug|Any CPU.Build.0 = Debug|Any CPU 87 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Release|Any CPU.Build.0 = Release|Any CPU 89 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 90 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Debug|Any CPU.Build.0 = Debug|Any CPU 91 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Release|Any CPU.ActiveCfg = Release|Any CPU 92 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Release|Any CPU.Build.0 = Release|Any CPU 93 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 94 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 95 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 96 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Release|Any CPU.Build.0 = Release|Any CPU 97 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 98 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Debug|Any CPU.Build.0 = Debug|Any CPU 99 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Release|Any CPU.ActiveCfg = Release|Any CPU 100 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Release|Any CPU.Build.0 = Release|Any CPU 101 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 102 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 103 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 104 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Release|Any CPU.Build.0 = Release|Any CPU 105 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 106 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 107 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 108 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Release|Any CPU.Build.0 = Release|Any CPU 109 | EndGlobalSection 110 | GlobalSection(SolutionProperties) = preSolution 111 | HideSolutionNode = FALSE 112 | EndGlobalSection 113 | GlobalSection(NestedProjects) = preSolution 114 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 115 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 116 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 117 | {8C6EDB84-1065-4643-BAFB-74E4992452B1} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 118 | {97232A73-765D-4E57-B8F7-178C5FD1F906} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 119 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928} = {6C54A309-86AB-4BCC-ACEE-6C400EEF180F} 120 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119} = {8D6B35D2-B87B-4A92-A42B-11C6B0612726} 121 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39} = {8B189B56-AD4C-4617-AC32-999567271C58} 122 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F} = {8D6B35D2-B87B-4A92-A42B-11C6B0612726} 123 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A} = {8D6B35D2-B87B-4A92-A42B-11C6B0612726} 124 | {8B189B56-AD4C-4617-AC32-999567271C58} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 125 | {3309A085-5888-4015-8382-4A7F1B493BDB} = {3C99F06C-4AD2-4328-B8C5-6988BF03F24A} 126 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA} = {3C99F06C-4AD2-4328-B8C5-6988BF03F24A} 127 | {D64C370E-4942-4A93-9F94-A142B38830DB} = {0E56099D-FEF0-4FBB-93E3-36B3F641DE42} 128 | {8D6B35D2-B87B-4A92-A42B-11C6B0612726} = {8B189B56-AD4C-4617-AC32-999567271C58} 129 | {6C54A309-86AB-4BCC-ACEE-6C400EEF180F} = {8B189B56-AD4C-4617-AC32-999567271C58} 130 | {0E56099D-FEF0-4FBB-93E3-36B3F641DE42} = {8B189B56-AD4C-4617-AC32-999567271C58} 131 | EndGlobalSection 132 | GlobalSection(ExtensibilityGlobals) = postSolution 133 | SolutionGuid = {C34924A5-C1CA-4D61-87AB-BD36772BA85E} 134 | EndGlobalSection 135 | EndGlobal 136 | -------------------------------------------------------------------------------- /Twilight.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True -------------------------------------------------------------------------------- /embold.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | sources: 4 | exclusions: 5 | - 'test' 6 | - 'generated' 7 | - 'mock' 8 | - 'thirdparty' 9 | - 'third-party' 10 | - '3rd-party' 11 | - '3rdparty' 12 | - 'external' 13 | - 'build' 14 | - 'node_modules' 15 | - 'assets' 16 | - 'gulp' 17 | - 'grunt' 18 | - 'library' 19 | - '.git' 20 | 21 | languages: 'C_SHARP' 22 | 23 | # Justification: No point checking ConfigureAwait() as .Net 6 has no context 24 | modules: 25 | - name: AwaitExpressionEustEndWithConfigureAwait 26 | enables: false 27 | --------------------------------------------------------------------------------