├── LICENSE ├── DependenciesGraph.png ├── Samples ├── Twilight.Samples.Common │ ├── IAssemblyMarker.cs │ ├── IRunner.cs │ ├── Views │ │ └── UserView.cs │ ├── Data │ │ ├── SampleDataContext.cs │ │ ├── ViewDataContext.cs │ │ └── Entities │ │ │ ├── UserEntity.cs │ │ │ └── UserViewEntity.cs │ ├── Features │ │ └── UserManagement │ │ │ ├── GetUserById │ │ │ ├── GetUserByIdValidator.cs │ │ │ └── GetUserByIdHandler.cs │ │ │ ├── GetUsersView │ │ │ ├── GetUsersViewValidator.cs │ │ │ └── GetUsersViewHandler.cs │ │ │ └── RegisterUser │ │ │ ├── RegisterUserValidator.cs │ │ │ ├── UserRegisteredEventValidator.cs │ │ │ ├── NotifyUserRegisteredHandler.cs │ │ │ ├── UserRegisteredHandler.cs │ │ │ └── RegisterUserCqrsCommandHandler.cs │ ├── AppHost.cs │ └── Twilight.Samples.Common.csproj └── Twilight.Samples.CQRS │ ├── DiagnosticsConfig.cs │ ├── AutofacModule.cs │ ├── Twilight.Samples.CQRS.csproj │ ├── Runner.cs │ └── Program.cs ├── Test ├── Twilight.CQRS.Benchmarks │ ├── MessageParameters.cs │ ├── Program.cs │ ├── Events │ │ ├── SendEventValidator.cs │ │ └── SendCqrsEventHandler.cs │ ├── Queries │ │ ├── SendQueryValidator.cs │ │ └── SendCqrsQueryHandler.cs │ ├── Commands │ │ ├── SendCommandValidator.cs │ │ └── SendCqrsCommandHandler.cs │ ├── AutofacModule.cs │ ├── Twilight.CQRS.Benchmarks.csproj │ ├── QuickBenchmarks.cs │ └── InMemoryBenchmarks.cs ├── Twilight.CQRS.Tests.Unit │ ├── Queries │ │ ├── TestQueryResponse.cs │ │ ├── TestQueryParametersValidator.cs │ │ ├── NonValidatingTestCqrsQueryHandler.cs │ │ ├── TestCqrsQueryHandler.cs │ │ ├── QueryHandlerTests.cs │ │ └── QueryTests.cs │ ├── TestParametersValidator.cs │ ├── Commands │ │ ├── TestCqrsCommandHandler.cs │ │ ├── CommandHandlerTests.cs │ │ └── CommandTests.cs │ ├── Twilight.CQRS.Tests.Unit.csproj │ └── Events │ │ └── EventTests.cs ├── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration │ ├── Setup │ │ ├── IVerifier.cs │ │ ├── ITestService.cs │ │ ├── MultipleHandlersParameters.cs │ │ ├── TestService.cs │ │ ├── IntegrationTestModule.cs │ │ ├── Handlers │ │ │ ├── TestCqrsEventHandler.cs │ │ │ ├── MultipleHandlersFirstHandler.cs │ │ │ ├── MultipleHandlersSecondHandler.cs │ │ │ ├── TestCqrsCommandHandler.cs │ │ │ ├── TestCqrsQueryHandler.cs │ │ │ └── TestCqrsCommandWithResponseHandler.cs │ │ ├── Validators.cs │ │ └── IntegrationTestBase.cs │ ├── AutofacDependencyResolutionFailureTests.cs │ ├── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.csproj │ ├── AutofacInMemoryMessageSenderFailureTests.cs │ └── AutofacInMemoryMessageSenderTests.cs ├── Twilight.CQRS.Tests.Common │ ├── TestParameters.cs │ ├── Twilight.CQRS.Tests.Common.csproj │ └── Constants.cs ├── Twilight.CQRS.Autofac.Tests.Unit │ ├── Setup │ │ └── TestCqrsCommandHandler.cs │ ├── Twilight.CQRS.Autofac.Tests.Unit.csproj │ └── CqrsRegistrationExtensionsTests.cs └── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit │ ├── AutofacInMemoryMessagingRegistrationExtensionsTests.cs │ └── Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit.csproj ├── Src ├── Twilight.CQRS.Interfaces │ ├── ICqrsEvent.cs │ ├── ICqrsQuery.cs │ ├── ICqrsEventHandler.cs │ ├── Twilight.CQRS.Interfaces.csproj │ ├── ICqrsCommand.cs │ ├── ICqrsQueryHandler.cs │ ├── ICqrsMessage.cs │ ├── ICqrsMessageHandler.cs │ ├── ICqrsCommandHandler.cs │ └── Twilight.CQRS.Interfaces.xml ├── Twilight.CQRS.Messaging.Interfaces │ ├── Twilight.CQRS.Messaging.Interfaces.csproj │ ├── IMessageSender.cs │ └── Twilight.CQRS.Messaging.Interfaces.xml ├── Twilight.CQRS.Messaging.InMemory.Autofac │ ├── AutofacInMemoryMessagingRegistrationExtensions.cs │ ├── Twilight.CQRS.Messaging.InMemory.Autofac.csproj │ ├── AutofacInMemoryMessageSenderLogs.cs │ ├── Twilight.CQRS.Messaging.InMemory.Autofac.xml │ └── AutofacInMemoryMessageSender.cs ├── Twilight.CQRS │ ├── CqrsMessage.cs │ ├── Queries │ │ ├── QueryResponse.cs │ │ ├── CqrsQuery.cs │ │ └── CqrsQueryHandlerBase.cs │ ├── Twilight.CQRS.csproj │ ├── Commands │ │ ├── CqrsCommandResponse.cs │ │ ├── CqrsCommand.cs │ │ └── CqrsCommandHandlerBase.cs │ ├── CqrsMessageHandler.cs │ └── Events │ │ ├── CqrsEvent.cs │ │ └── CqrsEventHandlerBase.cs └── Twilight.CQRS.Autofac │ ├── Twilight.CQRS.Autofac.csproj │ ├── Twilight.CQRS.Autofac.xml │ └── ContainerBuilderExtensions.cs ├── embold.yaml ├── Twilight.sln.DotSettings ├── .github └── workflows │ ├── codeql-analysis.yml │ └── dotnet.yml ├── .gitignore ├── README.md └── Twilight.sln /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verifiedcoder/Twilight/HEAD/LICENSE -------------------------------------------------------------------------------- /DependenciesGraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verifiedcoder/Twilight/HEAD/DependenciesGraph.png -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/IAssemblyMarker.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common; 2 | 3 | public interface IAssemblyMarker; 4 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/IRunner.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.Samples.Common; 2 | 3 | public interface IRunner 4 | { 5 | Task Run(); 6 | } 7 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/MessageParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Benchmarks; 2 | 3 | internal sealed record MessageParameters(string Message); 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/IVerifier.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 2 | 3 | public interface IVerifier 4 | { 5 | Task Receive(string parameter); 6 | } 7 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/ITestService.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 2 | 3 | public interface ITestService 4 | { 5 | Task Receive(string parameters); 6 | } 7 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/MultipleHandlersParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 2 | 3 | internal sealed class MultipleHandlersParameters 4 | { 5 | public string Value { get; } = "value"; 6 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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(); // or InMemoryBenchmarks 6 | 7 | Console.WriteLine(summary); 8 | Console.ReadLine(); 9 | 10 | Environment.Exit(0); 11 | -------------------------------------------------------------------------------- /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 ApplicationName = "Twilight.Samples.CQRS"; 8 | 9 | public static readonly ActivitySource ActivitySource = new(ApplicationName); 10 | } -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/TestService.cs: -------------------------------------------------------------------------------- 1 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 2 | 3 | internal sealed class TestService(IVerifier verifier) : ITestService 4 | { 5 | public async Task Receive(string parameters) 6 | => await verifier.Receive(parameters); 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/TestParametersValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | using Twilight.CQRS.Tests.Common; 3 | 4 | namespace Twilight.CQRS.Tests.Unit; 5 | 6 | internal sealed class TestParametersValidator : AbstractValidator> 7 | { 8 | public TestParametersValidator() 9 | => RuleFor(p => p.Params.Value).NotEmpty(); 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Common/Twilight.CQRS.Tests.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/TestQueryParametersValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Queries; 2 | using Twilight.CQRS.Tests.Common; 3 | 4 | namespace Twilight.CQRS.Tests.Unit.Queries; 5 | 6 | internal sealed class TestQueryParametersValidator : AbstractValidator>> 7 | { 8 | public TestQueryParametersValidator() 9 | => RuleFor(p => p.Params.Value).NotEmpty(); 10 | } 11 | -------------------------------------------------------------------------------- /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 | [MaxLength(256)] 11 | public string Forename { get; set; } = null!; 12 | 13 | [Required] 14 | [MaxLength(256)] 15 | public string Surname { get; set; } = null!; 16 | } 17 | -------------------------------------------------------------------------------- /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.Benchmarks/Events/SendEventValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Events; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Events; 4 | 5 | [UsedImplicitly] 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/Queries/SendQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Events; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Queries; 4 | 5 | [UsedImplicitly] 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/Commands/SendCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | 3 | namespace Twilight.CQRS.Benchmarks.Commands; 4 | 5 | [UsedImplicitly] 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 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/GetUserById/GetUserByIdValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Queries; 2 | 3 | namespace Twilight.Samples.Common.Features.UserManagement.GetUserById; 4 | 5 | [UsedImplicitly] 6 | public sealed class GetUserByIdValidator : AbstractValidator>> 7 | { 8 | public GetUserByIdValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.Params.UserId).GreaterThan(0); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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, ["Runner"]); 14 | } 15 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Interfaces/ICqrsQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Twilight.CQRS.Interfaces; 4 | 5 | /// 6 | /// Represents a message of type query with a response of arbitrary type. 7 | /// Implements . 8 | /// 9 | /// The type of the response payload. 10 | /// 11 | [SuppressMessage("Design", "S2326: Intentional design.")] 12 | public interface ICqrsQuery : ICqrsMessage; 13 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/GetUsersView/GetUsersViewValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Queries; 2 | 3 | namespace Twilight.Samples.Common.Features.UserManagement.GetUsersView; 4 | 5 | [UsedImplicitly] 6 | public sealed class GetUsersViewValidator : AbstractValidator>> 7 | { 8 | public GetUsersViewValidator() 9 | { 10 | RuleFor(p => p.Params).NotNull(); 11 | RuleFor(p => p.Params.RegistrationDate).LessThanOrEqualTo(DateTimeOffset.UtcNow); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/RegisterUser/RegisterUserValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | 3 | namespace Twilight.Samples.Common.Features.UserManagement.RegisterUser; 4 | 5 | [UsedImplicitly] 6 | public sealed class RegisterUserValidator : AbstractValidator> 7 | { 8 | public RegisterUserValidator() 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 | -------------------------------------------------------------------------------- /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 | builder.RegisterGeneric(typeof(NullLogger<>)).As(typeof(ILogger<>)).SingleInstance(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/RegisterUser/UserRegisteredEventValidator.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Events; 2 | 3 | namespace Twilight.Samples.Common.Features.UserManagement.RegisterUser; 4 | 5 | [UsedImplicitly] 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/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 | [MaxLength(256)] 14 | public string Forename { get; set; } = null!; 15 | 16 | [Required] 17 | [MaxLength(256)] 18 | public string Surname { get; set; } = null!; 19 | 20 | [Required] 21 | [MaxLength(513)] 22 | public string FullName { get; set; } = null!; 23 | 24 | [Required] 25 | public DateTimeOffset? RegistrationDate { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /Twilight.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True 4 | True 5 | True 6 | True -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Autofac.Tests.Unit/Setup/TestCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Messaging.Interfaces; 4 | using Twilight.CQRS.Tests.Common; 5 | 6 | namespace Twilight.CQRS.Autofac.Tests.Unit.Setup; 7 | 8 | internal sealed class TestCqrsCommandHandler( 9 | IMessageSender messageSender, 10 | ILogger logger, 11 | IValidator> validator) 12 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 13 | { 14 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 15 | => await Task.FromResult(Result.Ok()); 16 | } 17 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Events; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 6 | 7 | [UsedImplicitly] 8 | internal sealed class TestCqrsEventHandler( 9 | 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 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.Interfaces/Twilight.CQRS.Messaging.Interfaces.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | Twilight.CQRS.Messaging.Interfaces.xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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/Twilight.CQRS.Interfaces.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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/ICqrsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Twilight.CQRS.Interfaces; 4 | 5 | /// 6 | /// Represents a message of type command. 7 | /// Implements . 8 | /// 9 | /// 10 | public interface ICqrsCommand : ICqrsMessage; 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 | [SuppressMessage("Design", "S2326: Intentional design.")] 19 | 20 | public interface ICqrsCommand : ICqrsMessage; 21 | -------------------------------------------------------------------------------- /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 | if (logger.IsEnabled(LogLevel.Information)) 11 | { 12 | logger.LogInformation("Started {AppHost}.", nameof(AppHost)); 13 | } 14 | 15 | await runner.Run(); 16 | } 17 | 18 | public async Task StopAsync(CancellationToken cancellationToken) 19 | { 20 | if (logger.IsEnabled(LogLevel.Information)) 21 | { 22 | logger.LogInformation("Stopped {AppHost}.", nameof(AppHost)); 23 | } 24 | 25 | await Task.CompletedTask; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Events/SendCqrsEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Events; 3 | 4 | namespace Twilight.CQRS.Benchmarks.Events; 5 | 6 | internal sealed class SendCqrsEvent(MessageParameters parameters, string correlationId, string? causationId = null) : CqrsEvent(parameters, correlationId, causationId); 7 | 8 | [UsedImplicitly] 9 | internal sealed class SendCqrsEventHandler( 10 | ILogger logger, 11 | IValidator>? validator = null) : CqrsEventHandlerBase>(logger, validator) 12 | { 13 | public override async Task HandleEvent(CqrsEvent cqrsEvent, CancellationToken cancellationToken = default) 14 | => await Task.FromResult(Result.Ok()).ConfigureAwait(false); 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/MultipleHandlersFirstHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Messaging.Interfaces; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 6 | 7 | [UsedImplicitly] 8 | internal sealed class MultipleHandlersFirstHandler( 9 | IMessageSender messageSender, 10 | ILogger logger, 11 | IValidator> validator) : CqrsCommandHandlerBase>(messageSender, logger, validator) 12 | { 13 | public override Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 14 | => throw new NotImplementedException(); 15 | } 16 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/MultipleHandlersSecondHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Messaging.Interfaces; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 6 | 7 | [UsedImplicitly] 8 | internal sealed class MultipleHandlersSecondHandler( 9 | IMessageSender messageSender, 10 | ILogger logger, 11 | IValidator> validator) : CqrsCommandHandlerBase>(messageSender, logger, validator) 12 | { 13 | public override Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 14 | => throw new NotImplementedException(); 15 | } 16 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Messaging.Interfaces; 4 | using Twilight.CQRS.Tests.Common; 5 | 6 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 7 | 8 | [UsedImplicitly] 9 | internal sealed class TestCqrsCommandHandler( 10 | ITestService service, 11 | IMessageSender messageSender, 12 | ILogger logger, 13 | IValidator> validator) : CqrsCommandHandlerBase>(messageSender, logger, validator) 14 | { 15 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 16 | => await service.Receive(command.Params.Value); 17 | } 18 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/NonValidatingTestCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Queries; 3 | 4 | namespace Twilight.CQRS.Tests.Unit.Queries; 5 | 6 | [UsedImplicitly] 7 | internal sealed class NonValidatingTestCqrsQueryHandler( 8 | ILogger logger, 9 | IValidator>>? validator = null) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 10 | { 11 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 12 | { 13 | var response = new QueryResponse(string.Empty, query.CorrelationId, query.MessageId); 14 | 15 | return await Task.FromResult(Result.Ok(response)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Validators.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | using Twilight.CQRS.Events; 3 | using Twilight.CQRS.Queries; 4 | using Twilight.CQRS.Tests.Common; 5 | 6 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 7 | 8 | [UsedImplicitly] 9 | internal sealed class MultipleHandlersValidator : AbstractValidator>; 10 | 11 | [UsedImplicitly] 12 | internal sealed class TestCommandValidator : AbstractValidator>; 13 | 14 | [UsedImplicitly] 15 | internal sealed class TestCommandWithResponseValidator : AbstractValidator>>; 16 | 17 | [UsedImplicitly] 18 | internal sealed class TestEventValidator : AbstractValidator>; 19 | 20 | [UsedImplicitly] 21 | internal sealed class TestQueryValidator : AbstractValidator>>; 22 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/CqrsMessage.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Interfaces; 2 | 3 | namespace Twilight.CQRS; 4 | 5 | /// 6 | /// The message correlation identifier. 7 | /// The session identifier for the session to which this message belongs. 8 | /// 9 | /// The causation identifier. Identifies the message that caused this message to be produced. 10 | /// Optional. 11 | /// 12 | public abstract class CqrsMessage(string correlationId, string? sessionId = null, string? causationId = null) : ICqrsMessage 13 | { 14 | /// 15 | public string MessageId { get; } = Guid.NewGuid().ToString(); 16 | 17 | /// 18 | public string CorrelationId { get; } = correlationId; 19 | 20 | /// 21 | public string? SessionId { get; } = sessionId; 22 | 23 | /// 24 | public string? CausationId { get; } = causationId; 25 | } 26 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Commands/TestCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Events; 4 | using Twilight.CQRS.Messaging.Interfaces; 5 | using Twilight.CQRS.Tests.Common; 6 | 7 | namespace Twilight.CQRS.Tests.Unit.Commands; 8 | 9 | public sealed class TestCqrsCommandHandler( 10 | IMessageSender messageSender, 11 | ILogger logger, 12 | IValidator> validator) 13 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 14 | { 15 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 16 | { 17 | await MessageSender.Publish(new CqrsEvent(command.Params, command.CorrelationId, command.MessageId), cancellationToken); 18 | 19 | return await Task.FromResult(Result.Ok()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/TestCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Queries; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Tests.Unit.Queries; 6 | 7 | public sealed class TestCqrsQueryHandler( 8 | ILogger logger, 9 | IValidator>> validator) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 10 | { 11 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 12 | { 13 | var payload = new TestQueryResponse 14 | { 15 | Value = "1" 16 | }; 17 | 18 | var response = new QueryResponse(payload, query.CorrelationId, query.MessageId); 19 | 20 | return await Task.FromResult(Result.Ok(response)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 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: 10.x.x 34 | 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v3 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@v3 48 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Queries; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 6 | 7 | [UsedImplicitly] 8 | internal sealed class TestCqrsQueryHandler( 9 | ITestService service, 10 | ILogger logger, 11 | IValidator>> validator) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 12 | { 13 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 14 | { 15 | await service.Receive(query.Params.Value); 16 | 17 | var response = new QueryResponse(nameof(TestCqrsQueryHandler), query.CorrelationId, query.MessageId); 18 | 19 | return Result.Ok(response); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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.Autofac/Twilight.CQRS.Autofac.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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.Messaging.InMemory.Autofac/Twilight.CQRS.Messaging.InMemory.Autofac.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/RegisterUser/NotifyUserRegisteredHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Events; 3 | 4 | namespace Twilight.Samples.Common.Features.UserManagement.RegisterUser; 5 | 6 | public sealed record UserRegisteredParameters(int UserId, string Forename, string Surname); 7 | 8 | [UsedImplicitly] 9 | public sealed class NotifyUserRegisteredHandler( 10 | ILogger logger, 11 | IValidator> validator) 12 | : CqrsEventHandlerBase>(logger, validator) 13 | { 14 | public override async Task HandleEvent(CqrsEvent cqrsEvent, CancellationToken cancellationToken = default) 15 | { 16 | if (Logger.IsEnabled(LogLevel.Information)) 17 | { 18 | // Events can be used to trigger multiple business activities. 19 | Logger.LogInformation("Notify User Registered Handler: Handled Event, {EventTypeName}.", cqrsEvent.GetType().FullName); 20 | } 21 | 22 | return await Task.FromResult(Result.Ok()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 | /// The payload. 9 | /// The message correlation identifier. 10 | /// The session identifier. 11 | /// 12 | /// The causation identifier. Identifies the query that caused this response to be produced. 13 | /// Optional. 14 | /// 15 | /// 16 | public class QueryResponse( 17 | TPayload payload, 18 | string correlationId, 19 | string? sessionId = null, 20 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId) 21 | where TPayload : class 22 | { 23 | /// 24 | /// Gets the typed query response payload. 25 | /// 26 | /// The payload. 27 | public TPayload Payload { get; } = payload; 28 | } 29 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Twilight.CQRS.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | 14.0 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Twilight.Samples.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.0 6 | enable 7 | true 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/Handlers/TestCqrsCommandWithResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Messaging.Interfaces; 4 | using Twilight.CQRS.Tests.Common; 5 | 6 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup.Handlers; 7 | 8 | [UsedImplicitly] 9 | internal sealed class TestCqrsCommandWithResponseHandler( 10 | IMessageSender messageSender, 11 | ITestService service, 12 | ILogger logger, 13 | IValidator>> validator) : CqrsCommandHandlerBase>, CqrsCommandResponse>(messageSender, logger, validator) 14 | { 15 | protected override async Task>> HandleCommand(CqrsCommand> command, CancellationToken cancellationToken = default) 16 | { 17 | await service.Receive(command.Params.Value); 18 | 19 | var response = new CqrsCommandResponse(nameof(TestCqrsCommandWithResponseHandler), command.CorrelationId, command.MessageId); 20 | 21 | return response; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Setup/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Twilight.CQRS.Messaging.Interfaces; 3 | 4 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 5 | 6 | public abstract class IntegrationTestBase : IAsyncLifetime 7 | { 8 | private readonly IContainer _container; 9 | 10 | protected IntegrationTestBase() 11 | { 12 | Verifier = Substitute.For(); 13 | 14 | Verifier.Receive(Arg.Any()).Returns(Result.Ok()); 15 | 16 | var builder = new ContainerBuilder(); 17 | 18 | // Explicit variable type is required for service resolution. 19 | ITestService testService = new TestService(Verifier); 20 | 21 | builder.RegisterModule(); 22 | builder.RegisterInstance(testService); 23 | 24 | _container = builder.Build(); 25 | 26 | Subject = _container.Resolve(); 27 | } 28 | 29 | protected IMessageSender Subject { get; } 30 | 31 | protected IVerifier Verifier { get; } 32 | 33 | public async Task InitializeAsync() 34 | => await Task.CompletedTask; 35 | 36 | public async Task DisposeAsync() 37 | { 38 | _container.Dispose(); 39 | 40 | await Task.CompletedTask; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Commands/SendCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Events; 4 | using Twilight.CQRS.Messaging.Interfaces; 5 | 6 | namespace Twilight.CQRS.Benchmarks.Commands; 7 | 8 | internal sealed class SendCqrsCommand(MessageParameters parameters, string correlationId, string? causationId = null) : CqrsCommand(parameters, correlationId, causationId); 9 | 10 | internal sealed class SendCommandReceived(string correlationId, string? causationId = null) : CqrsEvent(correlationId, causationId); 11 | 12 | [UsedImplicitly] 13 | internal sealed class SendCqrsCommandHandler( 14 | IMessageSender messageSender, 15 | ILogger logger, 16 | IValidator>? validator = null) : CqrsCommandHandlerBase>(messageSender, logger, validator) 17 | { 18 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 19 | { 20 | var @event = new SendCommandReceived(command.CorrelationId, command.MessageId); 21 | 22 | await MessageSender.Publish(@event, cancellationToken).ConfigureAwait(false); 23 | 24 | return Result.Ok(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit/AutofacInMemoryMessagingRegistrationExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using Twilight.CQRS.Messaging.Interfaces; 5 | 6 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit; 7 | 8 | public sealed class AutofacInMemoryMessagingRegistrationExtensionsTests 9 | { 10 | [Fact] 11 | public void AddInMemoryMessaging_ShouldRegister_InMemoryMessageSender() 12 | { 13 | // Arrange 14 | var builder = new ContainerBuilder(); 15 | 16 | builder.AddAutofacInMemoryMessaging(); 17 | builder.RegisterType>().As>(); 18 | 19 | // Act 20 | var container = builder.Build(); 21 | 22 | // Assert 23 | container.ComponentRegistry.Registrations.Count().ShouldBe(3); 24 | 25 | container.ComponentRegistry 26 | .Registrations.Any(x => x.Services.Any(s => s.Description == typeof(IMessageSender).FullName)) 27 | .ShouldBeTrue(); 28 | 29 | var messageSender = container.Resolve(); 30 | 31 | messageSender.ShouldBeAssignableTo(); 32 | 33 | container.Dispose(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Twilight.CQRS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Commands/CommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Commands; 3 | using Twilight.CQRS.Events; 4 | using Twilight.CQRS.Messaging.Interfaces; 5 | using Twilight.CQRS.Tests.Common; 6 | 7 | namespace Twilight.CQRS.Tests.Unit.Commands; 8 | 9 | public sealed class CommandHandlerTests 10 | { 11 | private readonly IMessageSender _messageSender; 12 | private readonly TestCqrsCommandHandler _subject; 13 | 14 | public CommandHandlerTests() 15 | { 16 | // Setup 17 | _messageSender = Substitute.For(); 18 | 19 | var logger = Substitute.For>(); 20 | 21 | IValidator> validator = new TestParametersValidator(); 22 | 23 | _subject = new TestCqrsCommandHandler(_messageSender, logger, validator); 24 | } 25 | 26 | [Fact] 27 | public async Task Handler_ShouldPublishEvent_WhenHandling() 28 | { 29 | // Arrange 30 | var testCommand = new CqrsCommand(new TestParameters(), Constants.CorrelationId); 31 | 32 | // Act 33 | await _subject.HandleCommand(testCommand, CancellationToken.None); 34 | 35 | // Assert 36 | await _messageSender.Received(1).Publish(Arg.Any>(), Arg.Is(CancellationToken.None)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/Queries/SendCqrsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Queries; 3 | 4 | namespace Twilight.CQRS.Benchmarks.Queries; 5 | 6 | internal sealed class SendCqrsQuery(MessageParameters parameters, string correlationId, string? causationId = null) : CqrsQuery>(parameters, correlationId, causationId); 7 | 8 | internal sealed record QueryResponsePayload(string Response); 9 | 10 | [UsedImplicitly] 11 | internal sealed class SendCqrsQueryHandler( 12 | ILogger logger, 13 | IValidator>>? validator = null) : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 14 | { 15 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 16 | { 17 | var payload = new QueryResponsePayload("CqrsQuery Response"); 18 | var response = new QueryResponse(payload, query.CorrelationId, query.MessageId); 19 | 20 | return await Task.FromResult(Result.Ok(response)).ConfigureAwait(false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | dotnet-version: ['10.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@v4 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@v4 47 | with: 48 | path: buildOutput 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Autofac.Tests.Unit/Twilight.CQRS.Autofac.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Twilight.CQRS.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/RegisterUser/UserRegisteredHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Diagnostics; 3 | using Twilight.CQRS.Events; 4 | using Twilight.Samples.Common.Data; 5 | using Twilight.Samples.Common.Data.Entities; 6 | 7 | namespace Twilight.Samples.Common.Features.UserManagement.RegisterUser; 8 | 9 | [UsedImplicitly] 10 | public sealed class UserRegisteredHandler( 11 | 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(ActivityKind.Server)) 28 | { 29 | activity?.AddEvent(new ActivityEvent("Add User to View")); 30 | 31 | dataContext.UsersView.Add(userViewEntity); 32 | } 33 | 34 | await dataContext.SaveChangesAsync(cancellationToken); 35 | 36 | if (Logger.IsEnabled(LogLevel.Information)) 37 | { 38 | Logger.LogInformation("User Registered Handler: Handled Event, {EventTypeName}.", cqrsEvent.GetType().FullName); 39 | } 40 | 41 | return Result.Ok(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/AutofacDependencyResolutionFailureTests.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | using Twilight.CQRS.Tests.Common; 8 | 9 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration; 10 | 11 | public sealed class AutofacDependencyResolutionFailureTests : IAsyncLifetime 12 | { 13 | private static IContainer? _container; 14 | 15 | private readonly IMessageSender _subject; 16 | 17 | public AutofacDependencyResolutionFailureTests() 18 | { 19 | var builder = new ContainerBuilder(); 20 | 21 | builder.RegisterType().As(); 22 | builder.RegisterType>().As>(); 23 | 24 | _container = builder.Build(); 25 | 26 | _subject = _container.Resolve(); 27 | } 28 | 29 | public async Task InitializeAsync() 30 | => await Task.CompletedTask; 31 | 32 | public async Task DisposeAsync() 33 | { 34 | _container?.Dispose(); 35 | 36 | await Task.CompletedTask; 37 | } 38 | 39 | [Fact] 40 | public async Task MessageSender_ThrowsWhenDependencyResolutionFails() 41 | { 42 | // Arrange 43 | var parameters = new MultipleHandlersParameters(); 44 | var command = new CqrsCommand(parameters, Constants.CorrelationId); 45 | 46 | // Act 47 | var result = await _subject.Send(command, CancellationToken.None); 48 | 49 | // Assert 50 | result.IsSuccess.ShouldBe(false); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 | /// The payload. 24 | /// The message correlation identifier. 25 | /// 26 | /// The causation identifier. Identifies the command that caused this response to be produced. 27 | /// Optional. 28 | /// 29 | /// 30 | public class CqrsCommandResponse( 31 | TPayload payload, 32 | string correlationId, 33 | string? causationId = null) : CqrsMessage(correlationId, causationId) 34 | where TPayload : class 35 | { 36 | /// 37 | /// Gets the typed command response payload. 38 | /// 39 | /// The payload. 40 | public TPayload Payload { get; } = payload; 41 | } 42 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 14.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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/AutofacInMemoryMessageSenderFailureTests.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | using Twilight.CQRS.Events; 3 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 4 | using Twilight.CQRS.Tests.Common; 5 | 6 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration; 7 | 8 | public sealed class AutofacInMemoryMessageSenderFailureTests : IntegrationTestBase 9 | { 10 | [Fact] 11 | public async Task MessageSender_ThrowsWhenCommandHandlerDoesNotExist() 12 | { 13 | // Arrange 14 | var command = new CqrsCommand(Constants.CorrelationId); 15 | 16 | // Act 17 | var result = await Subject.Send(command, CancellationToken.None); 18 | 19 | // Assert 20 | result.IsSuccess.ShouldBeFalse(); 21 | result.Errors[0].Message.ShouldBe("No handler could be found for this request."); 22 | } 23 | 24 | [Fact] 25 | public async Task MessageSender_ThrowsWhenMultipleCommandHandlersResolved() 26 | { 27 | // Arrange 28 | var parameters = new MultipleHandlersParameters(); 29 | var command = new CqrsCommand(parameters, Constants.CorrelationId); 30 | 31 | // Act 32 | var result = await Subject.Send(command, CancellationToken.None); 33 | 34 | // Assert 35 | result.IsSuccess.ShouldBeFalse(); 36 | result.Errors[0].Message.ShouldBe("Multiple handlers found. A command may only have one handler."); 37 | } 38 | 39 | [Fact] 40 | public async Task MessageSender_ThrowsWhenNoHandlerRegisteredForEvent() 41 | { 42 | // Arrange 43 | var @event = new CqrsEvent(string.Empty, Constants.CorrelationId, Constants.CausationId); 44 | 45 | // Act 46 | var result = await Subject.Publish(@event, CancellationToken.None); 47 | 48 | // Assert 49 | result.IsSuccess.ShouldBeFalse(); 50 | result.Errors[0].Message.ShouldBe("No handler could be found for this request."); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Benchmarks/QuickBenchmarks.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 | // Do not seal this class 11 | [MemoryDiagnoser] 12 | [MinColumn, MaxColumn, MeanColumn, MedianColumn] 13 | [SimpleJob(warmupCount: 1, iterationCount: 3, invocationCount: 100)] 14 | public class QuickBenchmarks 15 | { 16 | private IMessageSender _messageSender = null!; 17 | private SendCqrsCommand _command = null!; 18 | private SendCqrsQuery _query = null!; 19 | private SendCqrsEvent _event = null!; 20 | 21 | [GlobalSetup] 22 | public void Setup() 23 | { 24 | var builder = new ContainerBuilder(); 25 | 26 | builder.RegisterModule(); 27 | 28 | var container = builder.Build(); 29 | 30 | _messageSender = container.Resolve(); 31 | 32 | // Pre-create objects to avoid allocation noise 33 | var commandParams = new MessageParameters("CqrsCommand"); 34 | 35 | _command = new SendCqrsCommand(commandParams, Guid.NewGuid().ToString()); 36 | 37 | var queryParams = new MessageParameters("CqrsQuery"); 38 | 39 | _query = new SendCqrsQuery(queryParams, Guid.NewGuid().ToString()); 40 | 41 | var eventParams = new MessageParameters("CqrsEvent"); 42 | 43 | _event = new SendCqrsEvent(eventParams, Guid.NewGuid().ToString()); 44 | } 45 | 46 | [Benchmark(Description = "Send Command")] 47 | public async Task SendCommand() 48 | => await _messageSender.Send(_command).ConfigureAwait(false); 49 | 50 | [Benchmark(Description = "Send Query")] 51 | public async Task SendQuery() 52 | => await _messageSender.Send(_query).ConfigureAwait(false); 53 | 54 | [Benchmark(Description = "Publish Event")] 55 | public async Task PublishEvent() 56 | => await _messageSender.Publish(_event).ConfigureAwait(false); 57 | } -------------------------------------------------------------------------------- /Src/Twilight.CQRS/CqrsMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using Twilight.CQRS.Interfaces; 4 | 5 | namespace Twilight.CQRS; 6 | 7 | /// 8 | /// The logger. 9 | /// The message validator. 10 | public abstract class CqrsMessageHandler( 11 | ILogger> logger, 12 | IValidator? validator = null) : ICqrsMessageHandler 13 | where TMessage : class 14 | { 15 | /// 16 | /// Gets the message handler logger. 17 | /// 18 | /// The logger. 19 | protected ILogger> Logger { get; } = logger; 20 | 21 | /// 22 | public virtual Task OnBeforeHandling(TMessage message, CancellationToken cancellationToken = default) 23 | => Task.FromResult(Result.Try(() => 24 | { 25 | Guard.IsNotNull(message); 26 | })); 27 | 28 | /// 29 | public virtual async Task ValidateMessage(TMessage message, CancellationToken cancellationToken = default) 30 | { 31 | if (validator is null) 32 | { 33 | return Result.Ok(); 34 | } 35 | 36 | var guardResult = Result.Try(() => 37 | { 38 | Guard.IsNotNull(message); 39 | }); 40 | 41 | if (guardResult.IsFailed) 42 | { 43 | return guardResult; 44 | } 45 | 46 | try 47 | { 48 | await validator.ValidateAndThrowAsync(message, cancellationToken); 49 | 50 | return Result.Ok(); 51 | } 52 | catch (ValidationException ex) 53 | { 54 | return Result.Fail(ex.Message); 55 | } 56 | } 57 | 58 | /// 59 | public virtual Task OnAfterHandling(TMessage message, CancellationToken cancellationToken = default) 60 | => Task.FromResult(Result.Try(() => 61 | { 62 | Guard.IsNotNull(message); 63 | })); 64 | } 65 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Autofac.Tests.Unit/CqrsRegistrationExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Twilight.CQRS.Autofac.Tests.Unit.Setup; 3 | 4 | namespace Twilight.CQRS.Autofac.Tests.Unit; 5 | 6 | public sealed class CqrsRegistrationExtensionsTests 7 | { 8 | private readonly ContainerBuilder _builder = new(); 9 | 10 | // Setup 11 | 12 | [Fact] 13 | public void CallingRegister_ForCqrsWithNullAssemblies_DoesNotThrow() 14 | { 15 | // Arrange / Act 16 | var subjectResult = () => { _builder.RegisterAssemblyTypes(); }; 17 | 18 | // Assert 19 | subjectResult.ShouldNotThrow(); 20 | } 21 | 22 | [Fact] 23 | public void RegisterForCqrs_RegistersAssemblyServices() 24 | { 25 | // Arrange 26 | var assembly = typeof(TestCqrsCommandHandler).Assembly; 27 | 28 | _builder.RegisterAssemblyTypes(assembly); 29 | 30 | // Act 31 | var container = _builder.Build(); 32 | 33 | // Assert 34 | container.ComponentRegistry.Registrations.Count().ShouldBe(4); 35 | container.ComponentRegistry.Registrations.ShouldBeUnique(); 36 | 37 | var services = (from r in container.ComponentRegistry.Registrations 38 | from s in r.Services 39 | select s.Description).ToList(); 40 | 41 | var expectedServices = new List 42 | { 43 | typeof(TestCqrsCommandHandler).Namespace ?? string.Empty 44 | }; 45 | 46 | AssertOnExpectedServices(expectedServices, services); 47 | } 48 | 49 | // ReSharper disable once SuggestBaseTypeForParameter as don't want multiple enumeration 50 | private static void AssertOnExpectedServices(IEnumerable expectedServices, List services) 51 | { 52 | foreach (var expectedService in expectedServices) 53 | { 54 | var selectedService = (from service in services 55 | where service.Contains(expectedService) 56 | select service).FirstOrDefault(); 57 | 58 | selectedService.ShouldNotBeNull(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/QueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Twilight.CQRS.Queries; 3 | using Twilight.CQRS.Tests.Common; 4 | 5 | namespace Twilight.CQRS.Tests.Unit.Queries; 6 | 7 | public sealed class QueryHandlerTests 8 | { 9 | private readonly TestCqrsQueryHandler _subject; 10 | 11 | // Setup 12 | public QueryHandlerTests() 13 | { 14 | var logger = Substitute.For>(); 15 | 16 | IValidator>> validator = new TestQueryParametersValidator(); 17 | 18 | _subject = new TestCqrsQueryHandler(logger, validator); 19 | } 20 | 21 | [Fact] 22 | public async Task Handler_ShouldHandleQuery() 23 | { 24 | // Arrange 25 | var testQuery = new CqrsQuery>(new TestParameters(), Constants.CorrelationId); 26 | 27 | // Act 28 | var response = await _subject.Handle(testQuery, CancellationToken.None); 29 | 30 | // Assert 31 | response.Value.Payload.Value.ShouldBe("1"); 32 | } 33 | 34 | [Fact] 35 | public async Task Handler_ShouldNotThrow_WhenValidatingValidQueryParameters() 36 | { 37 | // Arrange 38 | var testQuery = new CqrsQuery>(new TestParameters(), Constants.CorrelationId); 39 | 40 | // Act 41 | var subjectResult = async () => { await _subject.Handle(testQuery, CancellationToken.None); }; 42 | 43 | // Assert 44 | await subjectResult.ShouldNotThrowAsync(); 45 | } 46 | 47 | [Fact] 48 | public async Task Handler_ShouldReturnFailedResult_WhenValidatingInvalidQueryParameters() 49 | { 50 | // Arrange 51 | var testQuery = new CqrsQuery>(new TestParameters(string.Empty), Constants.CorrelationId); 52 | 53 | // Act 54 | var result = await _subject.Handle(testQuery, CancellationToken.None); 55 | 56 | // Assert 57 | result.IsFailed.ShouldBeTrue(); 58 | result.Errors.ShouldContain(error => error.Message.Contains("'Params Value' must not be empty")); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/GetUserById/GetUserByIdHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using Twilight.CQRS.Queries; 4 | using Twilight.Samples.Common.Data; 5 | using Twilight.Samples.Common.Data.Entities; 6 | 7 | namespace Twilight.Samples.Common.Features.UserManagement.GetUserById; 8 | 9 | public sealed record GetUserByIdParameters(int UserId); 10 | 11 | public sealed record GetUserByIdResponse(int UserId, string Forename, string Surname); 12 | 13 | [UsedImplicitly] 14 | public sealed class GetUserByIdHandler( 15 | ViewDataContext dataContext, 16 | ILogger logger, 17 | IValidator>> validator) 18 | : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 19 | { 20 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 21 | { 22 | UserViewEntity? userView; 23 | 24 | using (var activity = Activity.Current?.Source.StartActivity(ActivityKind.Server)) 25 | { 26 | activity?.AddEvent(new ActivityEvent("Get User by ID")); 27 | activity?.SetTag(nameof(GetUserByIdParameters.UserId), query.Params.UserId); 28 | 29 | userView = await dataContext.UsersView.FindAsync([query.Params.UserId], cancellationToken); 30 | } 31 | 32 | if (userView == null) 33 | { 34 | return Result.Fail($"User with Id '{query.Params.UserId}' not found."); 35 | } 36 | 37 | var payload = new GetUserByIdResponse(userView.Id, userView.Forename, userView.Surname); 38 | var response = new QueryResponse(payload, query.CorrelationId, null, query.MessageId); 39 | 40 | if (Logger.IsEnabled(LogLevel.Information)) 41 | { 42 | Logger.LogInformation("Handled CQRS Query, {QueryTypeName}.", query.GetType().FullName); 43 | } 44 | 45 | return await Task.FromResult(Result.Ok(response)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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 | // Do not seal this class 11 | [MemoryDiagnoser] 12 | [KeepBenchmarkFiles] 13 | public class InMemoryBenchmarks 14 | { 15 | private IMessageSender _messageSender = null!; 16 | 17 | [GlobalSetup] 18 | public void Setup() 19 | { 20 | var builder = new ContainerBuilder(); 21 | 22 | builder.RegisterModule(); 23 | 24 | var container = builder.Build(); 25 | 26 | _messageSender = container.Resolve(); 27 | } 28 | 29 | [Benchmark(Description = "Send 1000 Commands")] 30 | [Arguments(1000)] 31 | public async Task SendCommands(int count) 32 | { 33 | for (var i = 0; i < count; i++) 34 | { 35 | var parameters = new MessageParameters("CqrsCommand"); 36 | var command = new SendCqrsCommand(parameters, Guid.NewGuid().ToString()); 37 | 38 | await _messageSender.Send(command).ConfigureAwait(false); 39 | } 40 | } 41 | 42 | [Benchmark(Description = "Send 1000 Queries")] 43 | [Arguments(1000)] 44 | public async Task SendQueries(int count) 45 | { 46 | for (var i = 0; i < count; i++) 47 | { 48 | var parameters = new MessageParameters("CqrsQuery"); 49 | var query = new SendCqrsQuery(parameters, Guid.NewGuid().ToString()); 50 | 51 | await _messageSender.Send(query).ConfigureAwait(false); 52 | } 53 | } 54 | 55 | [Benchmark(Description = "Publish 1000 Events")] 56 | [Arguments(1000)] 57 | public async Task PublishEvents(int count) 58 | { 59 | for (var i = 0; i < count; i++) 60 | { 61 | var parameters = new MessageParameters("CqrsEvent"); 62 | var @event = new SendCqrsEvent(parameters, Guid.NewGuid().ToString()); 63 | 64 | await _messageSender.Publish(@event).ConfigureAwait(false); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/RegisterUser/RegisterUserCqrsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics; 4 | using Twilight.CQRS.Commands; 5 | using Twilight.CQRS.Events; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | using Twilight.Samples.Common.Data; 8 | using Twilight.Samples.Common.Data.Entities; 9 | 10 | namespace Twilight.Samples.Common.Features.UserManagement.RegisterUser; 11 | 12 | public sealed record RegisterUserCommandParameters(string Forename, string Surname); 13 | 14 | [UsedImplicitly] 15 | public sealed class RegisterUserCqrsCommandHandler( 16 | SampleDataContext context, 17 | IMessageSender messageSender, 18 | ILogger logger, 19 | IValidator> validator) 20 | : CqrsCommandHandlerBase>(messageSender, logger, validator) 21 | { 22 | public override async Task HandleCommand(CqrsCommand command, CancellationToken cancellationToken = default) 23 | { 24 | var userEntity = new UserEntity 25 | { 26 | Forename = command.Params.Forename, 27 | Surname = command.Params.Surname 28 | }; 29 | 30 | EntityEntry entityEntry; 31 | 32 | using (var activity = Activity.Current?.Source.StartActivity(ActivityKind.Server)) 33 | { 34 | activity?.AddEvent(new ActivityEvent("Register User")); 35 | 36 | entityEntry = context.Users.Add(userEntity); 37 | 38 | await context.SaveChangesAsync(cancellationToken); 39 | } 40 | 41 | var parameters = new UserRegisteredParameters(entityEntry.Entity.Id, command.Params.Forename, command.Params.Surname); 42 | var userRegisteredEvent = new CqrsEvent(parameters, command.CorrelationId, null, command.MessageId); 43 | 44 | if (Logger.IsEnabled(LogLevel.Information)) 45 | { 46 | Logger.LogInformation("Handled CQRS Command, {CommandTypeName}.", command.GetType().FullName); 47 | } 48 | 49 | await MessageSender.Publish(userRegisteredEvent, cancellationToken); 50 | 51 | return Result.Ok(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.Common/Features/UserManagement/GetUsersView/GetUsersViewHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Logging; 4 | using Twilight.CQRS.Queries; 5 | using Twilight.Samples.Common.Data; 6 | using Twilight.Samples.Common.Data.Entities; 7 | using Twilight.Samples.Common.Views; 8 | 9 | namespace Twilight.Samples.Common.Features.UserManagement.GetUsersView; 10 | 11 | public sealed record GetUsersViewParameters(DateTimeOffset RegistrationDate); 12 | 13 | public sealed record GetUsersViewResponse(IEnumerable Users); 14 | 15 | [UsedImplicitly] 16 | public sealed class GetUsersViewHandler( 17 | ViewDataContext dataContext, 18 | ILogger logger, 19 | IValidator>> validator) 20 | : CqrsQueryHandlerBase>, QueryResponse>(logger, validator) 21 | { 22 | protected override async Task>> HandleQuery(CqrsQuery> query, CancellationToken cancellationToken = default) 23 | { 24 | List? userViews; 25 | 26 | using (var activity = Activity.Current?.Source.StartActivity(ActivityKind.Server)) 27 | { 28 | activity?.AddEvent(new ActivityEvent("Get All Users")); 29 | 30 | userViews = await dataContext.UsersView.Where(u => u.RegistrationDate >= query.Params.RegistrationDate) 31 | .OrderBy(v => v.RegistrationDate) 32 | .ToListAsync(cancellationToken); 33 | } 34 | 35 | var usersView = userViews.Select(u => new UserView(u.Id, u.UserId, u.Forename, u.Surname, u.FullName, u.RegistrationDate)); 36 | 37 | var payload = new GetUsersViewResponse(usersView); 38 | var response = new QueryResponse(payload, query.CorrelationId, null, query.MessageId); 39 | 40 | if (Logger.IsEnabled(LogLevel.Information)) 41 | { 42 | Logger.LogInformation("Handled CQRS Query, {QueryTypeName}.", query.GetType().FullName); 43 | } 44 | 45 | return await Task.FromResult(Result.Ok(response)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/Twilight.Samples.CQRS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | 14.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 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.InMemory.Autofac/AutofacInMemoryMessageSenderLogs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Twilight.CQRS.Messaging.InMemory.Autofac; 4 | 5 | /// 6 | /// High-performance logging extension methods for . 7 | /// Uses LoggerMessage source generator to eliminate runtime overhead. 8 | /// 9 | internal static partial class AutofacInMemoryMessageSenderLogs 10 | { 11 | /// 12 | /// Logs a warning when no events are received for publishing. 13 | /// 14 | [LoggerMessage(LogLevel.Warning, "No events received for publishing when at least one event was expected. Check calls to publish.")] 15 | public static partial void LogNoEventsReceived(this ILogger logger); 16 | 17 | /// 18 | /// Logs a critical error when multiple handlers are found for a command. 19 | /// 20 | [LoggerMessage(LogLevel.Critical, "Multiple handlers found in {AssemblyQualifiedName}.")] 21 | public static partial void LogMultipleHandlersFound(this ILogger logger, string assemblyQualifiedName); 22 | 23 | /// 24 | /// Logs a critical error when handler resolution fails. 25 | /// 26 | [LoggerMessage(LogLevel.Critical, "No concrete handlers for type '{AssemblyQualifiedName}' could be resolved.")] 27 | public static partial void LogHandlerResolutionFailure(this ILogger logger, string assemblyQualifiedName, Exception ex); 28 | 29 | /// 30 | /// Logs a critical error when a component is not found in the DI container. 31 | /// 32 | [LoggerMessage(LogLevel.Critical, "No concrete handlers for type '{AssemblyQualifiedName}' could be found.")] 33 | public static partial void LogComponentNotFound(this ILogger logger, string assemblyQualifiedName, Exception ex); 34 | 35 | /// 36 | /// Logs a critical error when a handler is not found. 37 | /// 38 | [LoggerMessage(LogLevel.Critical, "Handler not found in '{AssemblyQualifiedName}'.")] 39 | public static partial void LogHandlerNotFoundCritical(this ILogger logger, string assemblyQualifiedName); 40 | 41 | /// 42 | /// Logs a critical error when handler execution fails. 43 | /// 44 | [LoggerMessage(LogLevel.Critical, "Could not execute handler for '{AssemblyQualifiedName}'.")] 45 | public static partial void LogHandlerExecutionFailure(this ILogger logger, string assemblyQualifiedName, Exception ex); 46 | } 47 | -------------------------------------------------------------------------------- /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 | /// The query correlation identifier. 14 | /// The session identifier. 15 | /// 16 | /// The causation identifier. Identifies the message that caused this query to be produced. 17 | /// Optional. 18 | /// 19 | /// 20 | /// 21 | public class CqrsQuery( 22 | string correlationId, 23 | string? sessionId = null, 24 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsQuery 25 | where TResponse : class; 26 | 27 | /// 28 | /// 29 | /// Represents a result and does not change the observable state of the system (i.e. is free of side effects). 30 | /// 31 | /// Implements . 32 | /// Implements . 33 | /// 34 | /// The type of the parameters. 35 | /// The type of the response. 36 | /// The parameters. 37 | /// The query correlation identifier. 38 | /// The session identifier. 39 | /// 40 | /// The causation identifier. Identifies the message that caused this query to be produced. 41 | /// Optional. 42 | /// 43 | /// 44 | /// 45 | public class CqrsQuery( 46 | TParameters parameters, 47 | string correlationId, 48 | string? sessionId = null, 49 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsQuery 50 | where TParameters : class 51 | where TResponse : class 52 | { 53 | /// 54 | /// Gets the typed query parameters. 55 | /// 56 | /// The parameters. 57 | public TParameters Params { get; } = parameters; 58 | } 59 | -------------------------------------------------------------------------------- /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 | /// The event correlation identifier. 16 | /// The session identifier. 17 | /// 18 | /// The causation identifier. Identifies the message that caused this event to be produced. 19 | /// Optional. 20 | /// 21 | /// 22 | /// 23 | public class CqrsEvent( 24 | string correlationId, 25 | string? sessionId = null, 26 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsEvent; 27 | 28 | /// 29 | /// 30 | /// Represents something that has already taken place in the domain. As such, always name an event with a 31 | /// past-participle verb, e.g. UserCreated. Events are facts and can be used to influence business decisions within 32 | /// the domain. Irrespective of whether an event has parameters or not, an event is always a 'fire-and-forget' 33 | /// operation and therefore does not return a response. 34 | /// 35 | /// Implements . 36 | /// Implements . 37 | /// 38 | /// The type of the parameters. 39 | /// The parameters. 40 | /// The event correlation identifier. 41 | /// The session identifier. 42 | /// 43 | /// The causation identifier. Identifies the message that caused this event to be produced. 44 | /// Optional. 45 | /// 46 | /// 47 | /// 48 | public class CqrsEvent( 49 | TParameters parameters, 50 | string correlationId, 51 | string? sessionId = null, 52 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsEvent 53 | where TParameters : class 54 | { 55 | /// 56 | /// Gets the typed event parameters. 57 | /// 58 | /// The parameters. 59 | public TParameters Params { get; } = parameters; 60 | } 61 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Events/EventTests.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Events; 2 | using Twilight.CQRS.Tests.Common; 3 | 4 | namespace Twilight.CQRS.Tests.Unit.Events; 5 | 6 | public sealed class EventTests 7 | { 8 | private readonly TestParameters _params = new(); 9 | 10 | [Fact] 11 | public void Event_WithoutParameters_ShouldAssignCausationId() 12 | { 13 | // Arrange / Act 14 | var subject = new CqrsEvent(Constants.CorrelationId, null, Constants.CausationId); 15 | 16 | // Assert 17 | subject.CausationId.ShouldBe(Constants.CausationId); 18 | } 19 | 20 | [Fact] 21 | public void Event_WithoutParameters_ShouldAssignCorrelationId() 22 | { 23 | // Arrange / Act 24 | var subject = new CqrsEvent(Constants.CorrelationId, null, Constants.CausationId); 25 | 26 | // Assert 27 | subject.CorrelationId.ShouldBe(Constants.CorrelationId); 28 | } 29 | 30 | [Fact] 31 | public void Event_WithParameters_ShouldAssignCausationId() 32 | { 33 | // Arrange / Act 34 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 35 | 36 | // Assert 37 | subject.CausationId.ShouldBe(Constants.CausationId); 38 | } 39 | 40 | [Fact] 41 | public void Event_WithParameters_ShouldAssignCorrelationId() 42 | { 43 | // Arrange / Act 44 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 45 | 46 | // Assert 47 | subject.CorrelationId.ShouldBe(Constants.CorrelationId); 48 | } 49 | 50 | [Fact] 51 | public void Event_WithParameters_ShouldAssignMessageId() 52 | { 53 | // Arrange / Act 54 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 55 | 56 | // Assert 57 | subject.MessageId.ShouldNotBeEmpty(); 58 | } 59 | 60 | [Fact] 61 | public void Event_WithParameters_ShouldAssignParameters() 62 | { 63 | // Arrange / Act 64 | var subject = new CqrsEvent(_params, Constants.CorrelationId, null, Constants.CausationId); 65 | 66 | // Assert 67 | subject.Params.ShouldNotBeNull(); 68 | subject.Params.ShouldBeEquivalentTo(_params); 69 | } 70 | 71 | [Fact] 72 | public void Query_WithParameters_AssignsSessionId() 73 | { 74 | // Arrange / Act 75 | var subject = new CqrsEvent(_params, Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 76 | 77 | //Assert 78 | subject.SessionId.ShouldBe(Constants.SessionId); 79 | } 80 | 81 | [Fact] 82 | public void Query_WithoutParameters_AssignsSessionId() 83 | { 84 | // Arrange / Act 85 | var subject = new CqrsEvent(Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 86 | 87 | //Assert 88 | subject.SessionId.ShouldBe(Constants.SessionId); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Commands/CommandTests.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | using Twilight.CQRS.Tests.Common; 3 | 4 | namespace Twilight.CQRS.Tests.Unit.Commands; 5 | 6 | public sealed class CommandTests 7 | { 8 | private readonly TestParameters _params = new(); 9 | 10 | [Fact] 11 | public void Command_WithoutParameters_ShouldAssignCausationId() 12 | { 13 | // arrange / Act 14 | var subject = new CqrsCommand(Constants.CorrelationId, Constants.CausationId); 15 | 16 | // Assert 17 | subject.CausationId.ShouldBe(Constants.CausationId); 18 | } 19 | 20 | [Fact] 21 | public void Command_WithoutParameters_ShouldAssignCorrelationId() 22 | { 23 | // Arrange / Act 24 | var subject = new CqrsCommand(Constants.CorrelationId, Constants.CausationId); 25 | 26 | //Assert 27 | subject.CorrelationId.ShouldBe(Constants.CorrelationId); 28 | } 29 | 30 | [Fact] 31 | public void Command_WithParameters_ShouldAssignCausationId() 32 | { 33 | // Arrange / Act 34 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 35 | 36 | // Assert 37 | subject.CausationId.ShouldBe(Constants.CausationId); 38 | } 39 | 40 | [Fact] 41 | public void Command_WithParameters_ShouldAssignCorrelationId() 42 | { 43 | // Arrange / Act 44 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 45 | 46 | // Assert 47 | subject.CorrelationId.ShouldBe(Constants.CorrelationId); 48 | } 49 | 50 | [Fact] 51 | public void Command_WithParameters_ShouldAssignMessageId() 52 | { 53 | // Arrange / Act 54 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 55 | 56 | subject.MessageId.ShouldNotBeEmpty(); 57 | } 58 | 59 | [Fact] 60 | public void Command_WithParameters_ShouldAssignParameters() 61 | { 62 | // Arrange / Act 63 | var subject = new CqrsCommand(_params, Constants.CorrelationId, null, Constants.CausationId); 64 | 65 | // Assert 66 | subject.Params.ShouldNotBeNull(); 67 | subject.Params.ShouldBeEquivalentTo(_params); 68 | } 69 | 70 | [Fact] 71 | public void Command_WithParameters_AssignsSessionId() 72 | { 73 | // Arrange / Act 74 | var subject = new CqrsCommand(_params, Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 75 | 76 | //Assert 77 | subject.SessionId.ShouldBe(Constants.SessionId); 78 | } 79 | 80 | [Fact] 81 | public void Command_WithoutParameters_AssignsSessionId() 82 | { 83 | // Arrange / Act 84 | var subject = new CqrsCommand(Constants.CorrelationId, Constants.CausationId, Constants.SessionId); 85 | 86 | //Assert 87 | subject.SessionId.ShouldBe(Constants.SessionId); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Tests.Unit/Queries/QueryTests.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Queries; 2 | using Twilight.CQRS.Tests.Common; 3 | 4 | namespace Twilight.CQRS.Tests.Unit.Queries; 5 | 6 | public sealed class QueryTests 7 | { 8 | private readonly TestParameters _params = new(); 9 | 10 | [Fact] 11 | public void Query_WithoutParameters_ShouldAssignCausationId() 12 | { 13 | // Arrange / Act 14 | var subject = new CqrsQuery(Constants.CorrelationId, null, Constants.CausationId); 15 | 16 | // Assert 17 | subject.CausationId.ShouldBe(Constants.CausationId); 18 | } 19 | 20 | [Fact] 21 | public void Query_WithoutParameters_ShouldAssignCorrelationId() 22 | { 23 | // Arrange / Act 24 | var subject = new CqrsQuery(Constants.CorrelationId, Constants.CausationId); 25 | 26 | // Assert 27 | subject.CorrelationId.ShouldBe(Constants.CorrelationId); 28 | } 29 | 30 | [Fact] 31 | public void Query_WithParameters_ShouldAssignCausationId() 32 | { 33 | // Arrange / Act 34 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 35 | 36 | // Assert 37 | subject.CausationId.ShouldBe(Constants.CausationId); 38 | } 39 | 40 | [Fact] 41 | public void Query_WithParameters_ShouldAssignCorrelationId() 42 | { 43 | // Arrange / Act 44 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 45 | 46 | // Assert 47 | subject.CorrelationId.ShouldBe(Constants.CorrelationId); 48 | } 49 | 50 | [Fact] 51 | public void Query_WithParameters_ShouldAssignMessageId() 52 | { 53 | // Arrange / Act 54 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 55 | 56 | // Assert 57 | subject.MessageId.ShouldNotBeEmpty(); 58 | } 59 | 60 | [Fact] 61 | public void Query_WithParameters_ShouldAssignParameters() 62 | { 63 | // Arrange / Act 64 | var subject = new CqrsQuery(_params, Constants.CorrelationId, null, Constants.CausationId); 65 | 66 | // Assert 67 | subject.Params.ShouldNotBeNull(); 68 | subject.Params.ShouldBeEquivalentTo(_params); 69 | } 70 | 71 | [Fact] 72 | public void Query_WithParameters_ShouldAssignSessionId() 73 | { 74 | // Arrange / Act 75 | var subject = new CqrsQuery(_params, Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 76 | 77 | // Assert 78 | subject.SessionId.ShouldBe(Constants.SessionId); 79 | } 80 | 81 | [Fact] 82 | public void Query_WithoutParameters_ShouldAssignSessionId() 83 | { 84 | // Arrange / Act 85 | var subject = new CqrsQuery(Constants.CorrelationId, Constants.SessionId, Constants.CausationId); 86 | 87 | // Assert 88 | subject.SessionId.ShouldBe(Constants.SessionId); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /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.Length == 0) 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.Count == 0) 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 | -------------------------------------------------------------------------------- /Samples/Twilight.Samples.CQRS/Runner.cs: -------------------------------------------------------------------------------- 1 | using FluentResults; 2 | using Serilog; 3 | using System.Diagnostics; 4 | using Taikandi; 5 | using Twilight.CQRS.Commands; 6 | using Twilight.CQRS.Messaging.Interfaces; 7 | using Twilight.CQRS.Queries; 8 | using Twilight.Samples.Common; 9 | using Twilight.Samples.Common.Features.UserManagement.GetUserById; 10 | using Twilight.Samples.Common.Features.UserManagement.GetUsersView; 11 | using Twilight.Samples.Common.Features.UserManagement.RegisterUser; 12 | 13 | namespace Twilight.Samples.CQRS; 14 | 15 | [UsedImplicitly] 16 | internal sealed class Runner(IMessageSender messageSender) : IRunner 17 | { 18 | public async Task Run() 19 | { 20 | await DemoRegisterUser(); 21 | await DemoGetRegisteredUser(); 22 | await DemoGetUsersView(); 23 | await DemoGetInvalidUsersView(); 24 | } 25 | 26 | private async Task DemoRegisterUser() 27 | { 28 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(DemoRegisterUser)}"); 29 | 30 | var id = GetActivityId(activity); 31 | var parameters = new RegisterUserCommandParameters("Bilbo", "Baggins"); 32 | var command = new CqrsCommand(parameters, id); 33 | 34 | await messageSender.Send(command); 35 | } 36 | 37 | private async Task DemoGetRegisteredUser() 38 | { 39 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(DemoGetRegisteredUser)}"); 40 | 41 | var id = GetActivityId(activity); 42 | var parameters = new GetUserByIdParameters(1); 43 | var query = new CqrsQuery>(parameters, id); 44 | 45 | var response = await messageSender.Send(query); 46 | 47 | Log.Information("Query response: {@GetRegisteredUserResponse}", response); 48 | } 49 | 50 | private async Task DemoGetUsersView() 51 | { 52 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(DemoGetUsersView)}"); 53 | 54 | var id = GetActivityId(activity); 55 | var parameters = new GetUsersViewParameters(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 | private async Task DemoGetInvalidUsersView() 64 | { 65 | using var activity = Activity.Current?.Source.StartActivity($"{nameof(DemoGetInvalidUsersView)}"); 66 | 67 | var id = GetActivityId(activity); 68 | var parameters = new GetUsersViewParameters(DateTimeOffset.UtcNow.AddDays(+1)); 69 | var query = new CqrsQuery>(parameters, id); 70 | 71 | var response = await messageSender.Send(query); 72 | 73 | if (response.IsFailed) 74 | { 75 | LogValidationErrors(response.Errors); 76 | } 77 | } 78 | 79 | private static string GetActivityId(Activity? activity) 80 | => activity?.Id ?? SequentialGuid.NewGuid().ToString(); 81 | 82 | private static void LogValidationErrors(IEnumerable errors) 83 | { 84 | Log.Error("Query validation failed:"); 85 | 86 | foreach (var error in errors) 87 | { 88 | Log.Error(" - {ValidationError}", error); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.Interfaces/IMessageSender.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Interfaces; 2 | 3 | namespace Twilight.CQRS.Messaging.Interfaces; 4 | 5 | /// 6 | /// Represents a means of dispatching messages. 7 | /// 8 | public interface IMessageSender 9 | { 10 | /// 11 | /// Runs the command handler registered for the given command type. 12 | /// 13 | /// Type of the command. 14 | /// Instance of the command. 15 | /// Task cancellation token. 16 | /// A Task that completes when the handler finished processing. 17 | Task Send(TCommand command, CancellationToken cancellationToken = default) 18 | where TCommand : class, ICqrsCommand; 19 | 20 | /// 21 | /// Runs the command handler registered for the given command type. 22 | /// 23 | /// 24 | /// This method should be implemented when a response (reply) to the originating service is required (i.e. the 25 | /// result of the command is fulfilled). It is recommended to restrain a command response to a scalar value. 26 | /// 27 | /// Type of the result. 28 | /// Instance of the command. 29 | /// Task cancellation token. 30 | /// A Task that resolves to a result of the command handler. 31 | Task> Send(ICqrsCommand command, CancellationToken cancellationToken = default); 32 | 33 | /// 34 | /// Runs the query handler registered for the given query type. 35 | /// 36 | /// 37 | /// This method should be implemented when a response (reply) to the originating service is required (i.e. the 38 | /// result of the query is fulfilled). 39 | /// 40 | /// Type of the result. 41 | /// Instance of the query. 42 | /// Task cancellation token. 43 | /// A Task that resolves to a result of the query handler. 44 | Task> Send(ICqrsQuery query, CancellationToken cancellationToken = default); 45 | 46 | /// 47 | /// Runs all registered event handlers for the specified events. 48 | /// 49 | /// The domain events. 50 | /// Task cancellation token. 51 | /// Task that completes when all handlers finish processing. 52 | Task Publish(IEnumerable events, CancellationToken cancellationToken = default) 53 | where TEvent : class, ICqrsEvent; 54 | 55 | /// 56 | /// Runs all registered event handlers for the specified event. 57 | /// 58 | /// Type of the event. 59 | /// Instance of the event. 60 | /// Task cancellation token. 61 | /// A Task that completes when all handlers finish processing. 62 | Task Publish(TEvent @event, CancellationToken cancellationToken = default) 63 | where TEvent : class, ICqrsEvent; 64 | } 65 | -------------------------------------------------------------------------------- /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 | const string applicationName = nameof(DiagnosticsConfig.ApplicationName); 17 | 18 | Log.Logger = new LoggerConfiguration().MinimumLevel.Verbose() 19 | .Enrich.WithProperty(nameof(DiagnosticsConfig.ApplicationName), applicationName) 20 | .WriteTo.Console(outputTemplate: consoleOutputTemplate) 21 | .CreateBootstrapLogger(); 22 | 23 | var builder = WebApplication.CreateBuilder(args); 24 | 25 | builder.Services.AddOpenTelemetry() 26 | .WithTracing(tracerProviderBuilder 27 | => tracerProviderBuilder.AddSource(DiagnosticsConfig.ActivitySource.Name) 28 | .ConfigureResource(resource => resource.AddService(applicationName)) 29 | .AddAspNetCoreInstrumentation() 30 | .AddConsoleExporter()) 31 | .WithMetrics(metricsProviderBuilder 32 | => metricsProviderBuilder.ConfigureResource(resource => resource.AddService(applicationName)) 33 | .AddAspNetCoreInstrumentation() 34 | .AddConsoleExporter()); 35 | 36 | builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()) 37 | .ConfigureContainer(containerBuilder => { containerBuilder.RegisterModule(); }) 38 | .UseSerilog((context, services, configuration) 39 | => configuration.ReadFrom.Configuration(context.Configuration) 40 | .ReadFrom.Services(services) 41 | .MinimumLevel.Verbose() 42 | .Enrich.WithProperty(nameof(DiagnosticsConfig.ApplicationName), applicationName) 43 | .WriteTo.Console(outputTemplate: consoleOutputTemplate)) 44 | .ConfigureServices(services => 45 | { 46 | services.AddDbContext(dbContextOptions => 47 | { 48 | dbContextOptions.UseInMemoryDatabase("DataDb"); 49 | dbContextOptions.EnableSensitiveDataLogging(); 50 | }); 51 | services.AddDbContext(dbContextOptions => 52 | { 53 | dbContextOptions.UseInMemoryDatabase("ViewDb"); 54 | dbContextOptions.EnableSensitiveDataLogging(); 55 | }); 56 | services.AddHostedService(); 57 | }); 58 | 59 | try 60 | { 61 | Log.Information("Starting {AppName}", applicationName); 62 | 63 | await builder.Build().RunAsync(); 64 | 65 | Log.Information("Running {AppName}", applicationName); 66 | } 67 | catch (Exception ex) 68 | { 69 | Log.Fatal(ex, "{AppName} terminated unexpectedly. Message: {ExceptionMessage}", applicationName, ex.Message); 70 | 71 | Environment.Exit(-1); 72 | } 73 | finally 74 | { 75 | Log.Information("Stopping {AppName}", applicationName); 76 | 77 | await Log.CloseAndFlushAsync(); 78 | 79 | Environment.Exit(0); 80 | } -------------------------------------------------------------------------------- /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 | 21 | Provides a means of dispatching messages. This implementation uses Autofac to resolve a registered message 22 | handler from the container and call that handler, passing any appropriate message. This class cannot be 23 | inherited. 24 | 25 | Implements . 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Provides an extension that uses Autofac to register in-memory messaging. 47 | 48 | 49 | 50 | 51 | Adds in-memory messaging using Autofac. 52 | 53 | The component registration builder. 54 | ContainerBuilder. 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /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 | /// The command correlation identifier. 15 | /// 16 | /// The causation identifier. Identifies the message that caused this command to be produced. 17 | /// Optional. 18 | /// 19 | /// The session identifier. 20 | /// 21 | /// 22 | public class CqrsCommand( 23 | string correlationId, 24 | string? causationId = null, 25 | string? sessionId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsCommand; 26 | 27 | /// 28 | /// 29 | /// Represents an action that does something and may carry a payload of arbitrary type 30 | /// . The command may carry parameters as a payload or not. Irrespective of 31 | /// whether a command has parameters or not, a command is always a 'fire-and-forget' operation and therefore should 32 | /// not return a response. 33 | /// 34 | /// Implements . 35 | /// Implements . 36 | /// 37 | /// The type of the parameters. 38 | /// The typed command parameters. 39 | /// The command correlation identifier. 40 | /// The session identifier. 41 | /// 42 | /// The causation identifier. Identifies the message that caused this command to be produced. 43 | /// Optional. 44 | /// 45 | /// 46 | /// 47 | public class CqrsCommand( 48 | TParameters parameters, 49 | string correlationId, 50 | string? sessionId = null, 51 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsCommand 52 | where TParameters : class 53 | { 54 | /// 55 | /// Gets the typed command parameters. 56 | /// 57 | /// The parameters. 58 | public TParameters Params { get; } = parameters; 59 | } 60 | 61 | /// 62 | /// 63 | /// Represents a result and does not change the observable state of the system (i.e. is free of side effects). 64 | /// 65 | /// Implements . 66 | /// Implements . 67 | /// 68 | /// The type of the parameters. 69 | /// The type of the response. 70 | /// The parameters. 71 | /// The command correlation identifier. 72 | /// The session identifier. 73 | /// 74 | /// The causation identifier. Identifies the message that caused this command to be produced. 75 | /// Optional. 76 | /// 77 | /// 78 | /// 79 | public class CqrsCommand( 80 | TParameters parameters, 81 | string correlationId, 82 | string? sessionId = null, 83 | string? causationId = null) : CqrsMessage(correlationId, sessionId, causationId), ICqrsCommand 84 | where TParameters : class 85 | where TResponse : class 86 | { 87 | /// 88 | /// Gets the typed command parameters. 89 | /// 90 | /// The parameters. 91 | public TParameters Params { get; } = parameters; 92 | } 93 | -------------------------------------------------------------------------------- /Test/Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration/AutofacInMemoryMessageSenderTests.cs: -------------------------------------------------------------------------------- 1 | using Twilight.CQRS.Commands; 2 | using Twilight.CQRS.Events; 3 | using Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration.Setup; 4 | using Twilight.CQRS.Queries; 5 | using Twilight.CQRS.Tests.Common; 6 | 7 | namespace Twilight.CQRS.Messaging.InMemory.Autofac.Tests.Integration; 8 | 9 | public sealed class AutofacInMemoryMessageSenderTests : IntegrationTestBase 10 | { 11 | [Fact] 12 | public async Task MessageSender_CallsCorrectHandler_ForCommand() 13 | { 14 | // Arrange 15 | var command = new CqrsCommand(new TestParameters(nameof(MessageSender_CallsCorrectHandler_ForCommand)), Constants.CorrelationId); 16 | 17 | // Act 18 | await Subject.Send(command, CancellationToken.None); 19 | 20 | // Assert 21 | await Verifier.Received(1).Receive(Arg.Is(command.Params.Value)); 22 | } 23 | 24 | [Fact] 25 | public async Task MessageSender_CallsCorrectHandler_ForCommand_WithResponse() 26 | { 27 | // Arrange 28 | var command = new CqrsCommand>(new TestParameters(nameof(MessageSender_CallsCorrectHandler_ForCommand_WithResponse)), Constants.CorrelationId); 29 | 30 | // Act 31 | var result = await Subject.Send(command, CancellationToken.None); 32 | 33 | // Assert 34 | await Verifier.Received(1).Receive(Arg.Is(command.Params.Value)); 35 | 36 | result.Value.ShouldNotBeNull(); 37 | result.Value.CorrelationId.ShouldBe(Constants.CorrelationId); 38 | } 39 | 40 | [Fact] 41 | public async Task MessageSender_CallsCorrectHandler_ForEvent() 42 | { 43 | // Arrange 44 | var @event = new CqrsEvent(new TestParameters(nameof(MessageSender_CallsCorrectHandler_ForEvent)), Constants.CorrelationId, Constants.CausationId); 45 | 46 | // Act 47 | await Subject.Publish(@event, CancellationToken.None); 48 | 49 | // Assert 50 | await Verifier.Received(1).Receive(Arg.Is(@event.Params.Value)); 51 | } 52 | 53 | [Fact] 54 | public async Task MessageSender_CallsCorrectHandler_ForEvents() 55 | { 56 | // Arrange 57 | var @event = new CqrsEvent(new TestParameters(nameof(MessageSender_CallsCorrectHandler_ForEvents)), Constants.CorrelationId, Constants.CausationId); 58 | var events = new List> 59 | { 60 | @event 61 | }; 62 | 63 | var enumerableEvents = events.AsEnumerable(); 64 | 65 | // Act 66 | await Subject.Publish(enumerableEvents, CancellationToken.None); 67 | 68 | // Assert 69 | await Verifier.Received(1).Receive(@event.Params.Value); 70 | } 71 | 72 | [Fact] 73 | public async Task MessageSender_CallsCorrectHandler_ForQuery() 74 | { 75 | // Arrange 76 | var query = new CqrsQuery>(new TestParameters(nameof(MessageSender_CallsCorrectHandler_ForQuery)), Constants.CorrelationId); 77 | 78 | // Act 79 | var result = await Subject.Send(query, CancellationToken.None); 80 | 81 | // Assert 82 | await Verifier.Received(1).Receive(Arg.Is(query.Params.Value)); 83 | 84 | result.Value.ShouldNotBeNull(); 85 | result.Value.CorrelationId.ShouldBe(Constants.CorrelationId); 86 | } 87 | 88 | [Fact] 89 | public async Task MessageSender_ThrowsWhenCommandHandlerIsNotFound() 90 | { 91 | // Arrange 92 | var command = new CqrsCommand(string.Empty, Constants.CorrelationId, Constants.CausationId); 93 | 94 | // Act 95 | var result = await Subject.Send(command, CancellationToken.None); 96 | 97 | // Assert 98 | result.IsSuccess.ShouldBeFalse(); 99 | result.Errors[0].Message.ShouldBe("No handler could be found for this request."); 100 | } 101 | 102 | [Fact] 103 | public async Task MessageSender_ThrowsWhenEventHandlerIsNotFound() 104 | { 105 | // Arrange 106 | var @event = new CqrsEvent(string.Empty, Constants.CorrelationId, Constants.CausationId); 107 | 108 | // Act 109 | var result = await Subject.Publish(@event, CancellationToken.None); 110 | 111 | // Assert 112 | result.IsSuccess.ShouldBeFalse(); 113 | result.Errors[0].Message.ShouldBe("No handler could be found for this request."); 114 | } 115 | 116 | [Fact] 117 | public async Task MessageSender_ThrowsWhenQueryHandlerIsNotFound() 118 | { 119 | // Arrange 120 | var query = new CqrsQuery>(string.Empty, Constants.CorrelationId); 121 | 122 | // Act 123 | var result = await Subject.Send(query, CancellationToken.None); 124 | 125 | // Assert 126 | result.IsSuccess.ShouldBeFalse(); 127 | result.Errors[0].Message.ShouldBe("No handler could be found for this request."); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Events/CqrsEventHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics; 4 | using Twilight.CQRS.Interfaces; 5 | // ReSharper disable ExplicitCallerInfoArgument as false positive for StartActivity 6 | 7 | namespace Twilight.CQRS.Events; 8 | 9 | /// 10 | /// 11 | /// Represents the ability to process (handle) events. An event handler receives a published event and 12 | /// brokers a result. A result is either a successful consumption of the event, or an exception. Events can be 13 | /// consumed by multiple event handlers. This class cannot be instantiated. 14 | /// 15 | /// Implements . 16 | /// Implements . 17 | /// 18 | /// The type of the event. 19 | /// The type of the event handler. 20 | /// The logger. 21 | /// The event validator. 22 | /// 23 | /// 24 | public abstract class CqrsEventHandlerBase( 25 | ILogger> logger, 26 | IValidator? validator = null) : CqrsMessageHandler(logger, validator), ICqrsEventHandler 27 | where TEvent : class, ICqrsEvent 28 | { 29 | /// 30 | public async Task Handle(TEvent @event, CancellationToken cancellationToken = default) 31 | { 32 | var guardResult = Result.Try(() => Guard.IsNotNull(@event)); 33 | 34 | if (guardResult.IsFailed) 35 | { 36 | return guardResult; 37 | } 38 | 39 | using var activity = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity($"Handle {@event.GetType()}") : null; 40 | 41 | var preHandlingResult = await ExecutePreHandlingAsync(@event, cancellationToken); 42 | 43 | if (!preHandlingResult.IsSuccess) 44 | { 45 | return preHandlingResult; 46 | } 47 | 48 | var validationResult = await ExecuteValidationAsync(@event, cancellationToken); 49 | 50 | if (!validationResult.IsSuccess) 51 | { 52 | return validationResult; 53 | } 54 | 55 | var eventResult = await ExecuteHandleEventAsync(@event, cancellationToken); 56 | 57 | if (!eventResult.IsSuccess) 58 | { 59 | return eventResult; 60 | } 61 | 62 | var postHandlingResult = await ExecutePostHandlingAsync(@event, cancellationToken); 63 | 64 | return !postHandlingResult.IsSuccess 65 | ? eventResult 66 | : postHandlingResult; 67 | } 68 | 69 | private static bool ShouldCreateActivity() => Activity.Current?.Source.HasListeners() ?? false; 70 | 71 | /// 72 | /// Handles the event. 73 | /// 74 | /// The event. 75 | /// The cancellation token. 76 | /// A task that represents the asynchronous handle event operation. 77 | public abstract Task HandleEvent(TEvent @event, CancellationToken cancellationToken = default); 78 | 79 | private async Task ExecutePreHandlingAsync(TEvent @event, CancellationToken cancellationToken) 80 | { 81 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Pre event handling logic") : null; 82 | 83 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase<,>)}.{nameof(OnBeforeHandling)}")); 84 | 85 | return await OnBeforeHandling(@event, cancellationToken); 86 | } 87 | 88 | private async Task ExecuteValidationAsync(TEvent @event, CancellationToken cancellationToken) 89 | { 90 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Validate event") : null; 91 | 92 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase<,>)}.{nameof(ValidateMessage)}")); 93 | 94 | return await ValidateMessage(@event, cancellationToken); 95 | } 96 | 97 | private async Task ExecuteHandleEventAsync(TEvent @event, CancellationToken cancellationToken) 98 | { 99 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Handle event") : null; 100 | 101 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase<,>)}.{nameof(HandleEvent)}")); 102 | 103 | return await HandleEvent(@event, cancellationToken); 104 | } 105 | 106 | private async Task ExecutePostHandlingAsync(TEvent @event, CancellationToken cancellationToken) 107 | { 108 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Post event handling logic") : null; 109 | 110 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsEventHandlerBase<,>)}.{nameof(OnAfterHandling)}")); 111 | 112 | return await OnAfterHandling(@event, cancellationToken); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Queries/CqrsQueryHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics; 4 | using Twilight.CQRS.Interfaces; 5 | // ReSharper disable ExplicitCallerInfoArgument as false positive for StartActivity 6 | 7 | namespace Twilight.CQRS.Queries; 8 | 9 | /// 10 | /// 11 | /// Represents the ability to process (handle) queries. A query handler receives a query and directs the query 12 | /// payload for processing. This class cannot be instantiated. 13 | /// 14 | /// Implements . 15 | /// Implements . 16 | /// 17 | /// The type of the query. 18 | /// The type of the query response. 19 | /// The type of the query handler. 20 | /// The logger. 21 | /// The query validator. 22 | /// 23 | /// 24 | public abstract class CqrsQueryHandlerBase( 25 | ILogger> logger, 26 | IValidator? validator = null) : CqrsMessageHandler(logger, validator), ICqrsQueryHandler 27 | where TQuery : class, ICqrsQuery 28 | { 29 | /// 30 | public async Task> Handle(TQuery query, CancellationToken cancellationToken = default) 31 | { 32 | var guardResult = Result.Try(() => Guard.IsNotNull(query)); 33 | 34 | if (guardResult.IsFailed) 35 | { 36 | return guardResult; 37 | } 38 | 39 | using var activity = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity($"Handle {query.GetType()}") : null; 40 | 41 | var preHandlingResult = await ExecutePreHandlingAsync(query, cancellationToken); 42 | 43 | if (!preHandlingResult.IsSuccess) 44 | { 45 | return preHandlingResult; 46 | } 47 | 48 | var validationResult = await ExecuteValidationAsync(query, cancellationToken); 49 | 50 | if (!validationResult.IsSuccess) 51 | { 52 | return validationResult; 53 | } 54 | 55 | var queryResult = await ExecuteHandleQueryAsync(query, cancellationToken); 56 | 57 | if (!queryResult.IsSuccess) 58 | { 59 | return queryResult; 60 | } 61 | 62 | var postHandlingResult = await ExecutePostHandlingAsync(query, cancellationToken); 63 | 64 | return postHandlingResult.IsSuccess 65 | ? queryResult 66 | : postHandlingResult; 67 | } 68 | 69 | private static bool ShouldCreateActivity() => Activity.Current?.Source.HasListeners() ?? false; 70 | 71 | /// 72 | /// Handles the query. 73 | /// 74 | /// The query. 75 | /// The cancellation token. 76 | /// 77 | /// A task that represents the asynchronous query handler operation. 78 | /// The task result contains the query execution response. 79 | /// 80 | protected abstract Task> HandleQuery(TQuery query, CancellationToken cancellationToken = default); 81 | 82 | private async Task> ExecutePreHandlingAsync(TQuery query, CancellationToken cancellationToken) 83 | { 84 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Pre query handling logic") : null; 85 | 86 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase<,,>)}.{nameof(OnBeforeHandling)}")); 87 | 88 | return await OnBeforeHandling(query, cancellationToken); 89 | } 90 | 91 | private async Task> ExecuteValidationAsync(TQuery query, CancellationToken cancellationToken) 92 | { 93 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Validate query") : null; 94 | 95 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase<,,>)}.{nameof(ValidateMessage)}")); 96 | 97 | return await ValidateMessage(query, cancellationToken); 98 | } 99 | 100 | private async Task> ExecuteHandleQueryAsync(TQuery query, CancellationToken cancellationToken) 101 | { 102 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Handle query") : null; 103 | 104 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase<,,>)}.{nameof(HandleQuery)}")); 105 | 106 | return await HandleQuery(query, cancellationToken); 107 | } 108 | 109 | private async Task> ExecutePostHandlingAsync(TQuery query, CancellationToken cancellationToken) 110 | { 111 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Post query handling logic") : null; 112 | 113 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsQueryHandlerBase<,,>)}.{nameof(OnAfterHandling)}")); 114 | 115 | return await OnAfterHandling(query, cancellationToken); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilight 2 | 3 | See the [LICENSE](LICENSE) file for legal guidance on using the code in this repository. The MIT license 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 version of .Net. 10 | 11 | ### Requirements 12 | 13 | - Microsoft Visual Studio 2026+ 14 | - .NET 10.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). However, 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 identifier that identifies a series of actions in a workflow. In addition, there is a causation identifier. The causation identifier specifies which previous 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 distributed systems. 49 | 50 | Messages form the basis of CQRS commands, queries and events. A command, query or event is a message: something you send to, or make abvailable 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 introducing 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 | ## Should I Use This? 102 | If you're not I/O constrained, and only need basic pipeline functionality, then yes, this library is a good fit. It provides a simple way to implement CQRS without the overhead of more complex libraries. And it's fast. Very fast. 103 | 104 | ``` 105 | BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat) 106 | Intel Core Ultra 7 265K 3.88GHz, 1 CPU, 16 logical and 16 physical cores 107 | .NET SDK 10.0.100-rc.2.25502.107 108 | [Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3 109 | DefaultJob : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3 110 | ``` 111 | | Method | Count | Mean | Error | StdDev | Gen0 | Allocated | 112 | |-------------------- |------ |---------:|--------:|--------:|---------:|----------:| 113 | | Send 1000 Commands | 1000 | 695.8 μs | 5.42 μs | 4.81 μs | 154.2969 | 2.31 MB | 114 | | Send 1000 Queries | 1000 | 560.2 μs | 3.51 μs | 3.28 μs | 80.0781 | 1.2 MB | 115 | | Publish 1000 Events | 1000 | 666.3 μs | 4.52 μs | 4.23 μs | 150.3906 | 2.25 MB | 116 | 117 | Observed throughput: 118 | 119 | - Send Command: 0.000696ms (0.696µs) 120 | - Send Query: 0.000560ms (0.560µs) 121 | - Publish Event: 0.000666ms (0.666µs) 122 | 123 | ## Naming Twilight 124 | 125 | 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 Brighter and Darker, Twilight, stuck with attendant *"can you see what I did there?"* gusto. 126 | 127 | The Brighter and Darker projects are excellent examples of how complicated the topic of CQRS can get! 128 | 129 | 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. 130 | 131 | ## Sources 132 | 133 | 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: 134 | 135 | - [https://github.com/jbogard/MediatR](https://github.com/jbogard/MediatR) 136 | - [https://martinfowler.com/bliki/CQRS.html](https://martinfowler.com/bliki/CQRS.html) 137 | - [https://martinfowler.com/eaaDev/EventSourcing.html](https://martinfowler.com/eaaDev/EventSourcing.html) 138 | 139 | ## Note 140 | 141 | Twilight incorporates [Open Telemetry](https://opentelemetry.io/). A console exporter has been added to the sample so you can view the telemetry. -------------------------------------------------------------------------------- /Twilight.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 18 4 | VisualStudioVersion = 18.3.11206.111 d18.3 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 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C75D2542-C555-4524-86D4-67AE7E518C09}" 52 | EndProject 53 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{A269337C-D516-4F23-8BE8-EC6DDE7DE90F}" 54 | ProjectSection(SolutionItems) = preProject 55 | .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml 56 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml 57 | EndProjectSection 58 | EndProject 59 | Global 60 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 61 | Debug|Any CPU = Debug|Any CPU 62 | Release|Any CPU = Release|Any CPU 63 | EndGlobalSection 64 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 65 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {8C6EDB84-1065-4643-BAFB-74E4992452B1}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 82 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Debug|Any CPU.Build.0 = Debug|Any CPU 83 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {97232A73-765D-4E57-B8F7-178C5FD1F906}.Release|Any CPU.Build.0 = Release|Any CPU 85 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 86 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Debug|Any CPU.Build.0 = Debug|Any CPU 87 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928}.Release|Any CPU.Build.0 = Release|Any CPU 89 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 90 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Debug|Any CPU.Build.0 = Debug|Any CPU 91 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Release|Any CPU.ActiveCfg = Release|Any CPU 92 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119}.Release|Any CPU.Build.0 = Release|Any CPU 93 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 94 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Debug|Any CPU.Build.0 = Debug|Any CPU 95 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Release|Any CPU.ActiveCfg = Release|Any CPU 96 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39}.Release|Any CPU.Build.0 = Release|Any CPU 97 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 98 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Debug|Any CPU.Build.0 = Debug|Any CPU 99 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Release|Any CPU.ActiveCfg = Release|Any CPU 100 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F}.Release|Any CPU.Build.0 = Release|Any CPU 101 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 102 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 103 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 104 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A}.Release|Any CPU.Build.0 = Release|Any CPU 105 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 106 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Debug|Any CPU.Build.0 = Debug|Any CPU 107 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Release|Any CPU.ActiveCfg = Release|Any CPU 108 | {3309A085-5888-4015-8382-4A7F1B493BDB}.Release|Any CPU.Build.0 = Release|Any CPU 109 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 110 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 111 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 112 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA}.Release|Any CPU.Build.0 = Release|Any CPU 113 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 114 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 115 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 116 | {D64C370E-4942-4A93-9F94-A142B38830DB}.Release|Any CPU.Build.0 = Release|Any CPU 117 | EndGlobalSection 118 | GlobalSection(SolutionProperties) = preSolution 119 | HideSolutionNode = FALSE 120 | EndGlobalSection 121 | GlobalSection(NestedProjects) = preSolution 122 | {FBF4A6DF-A683-4960-AF89-87079CBAAEC5} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 123 | {DD65D5D3-6FBD-4709-B4FE-7BC2F912190A} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 124 | {E7F1DF13-5089-41CE-8D5E-D461E99CB426} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 125 | {8C6EDB84-1065-4643-BAFB-74E4992452B1} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 126 | {97232A73-765D-4E57-B8F7-178C5FD1F906} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 127 | {143C2D4D-66F9-4C8A-8758-4BC5BCF3F928} = {6C54A309-86AB-4BCC-ACEE-6C400EEF180F} 128 | {5E69A57A-B4BF-4C25-82AD-A23CE049C119} = {8D6B35D2-B87B-4A92-A42B-11C6B0612726} 129 | {EB3DEE27-091D-40DB-9BAC-AFFF049CFD39} = {8B189B56-AD4C-4617-AC32-999567271C58} 130 | {01860F80-5AD9-4251-B0F4-6DE9507DA39F} = {8D6B35D2-B87B-4A92-A42B-11C6B0612726} 131 | {18337CF2-C3EA-4201-A597-DC9649D4EB2A} = {8D6B35D2-B87B-4A92-A42B-11C6B0612726} 132 | {8B189B56-AD4C-4617-AC32-999567271C58} = {8C3A64F8-54D3-4EFA-B910-2DDE79D7206C} 133 | {3309A085-5888-4015-8382-4A7F1B493BDB} = {3C99F06C-4AD2-4328-B8C5-6988BF03F24A} 134 | {15E6B609-AAD2-4AD1-AB57-3ABC53A268FA} = {3C99F06C-4AD2-4328-B8C5-6988BF03F24A} 135 | {D64C370E-4942-4A93-9F94-A142B38830DB} = {0E56099D-FEF0-4FBB-93E3-36B3F641DE42} 136 | {8D6B35D2-B87B-4A92-A42B-11C6B0612726} = {8B189B56-AD4C-4617-AC32-999567271C58} 137 | {6C54A309-86AB-4BCC-ACEE-6C400EEF180F} = {8B189B56-AD4C-4617-AC32-999567271C58} 138 | {0E56099D-FEF0-4FBB-93E3-36B3F641DE42} = {8B189B56-AD4C-4617-AC32-999567271C58} 139 | {C75D2542-C555-4524-86D4-67AE7E518C09} = {7F80EF36-698E-4D2C-9F28-86684EC5DF4A} 140 | {A269337C-D516-4F23-8BE8-EC6DDE7DE90F} = {C75D2542-C555-4524-86D4-67AE7E518C09} 141 | EndGlobalSection 142 | GlobalSection(ExtensibilityGlobals) = postSolution 143 | SolutionGuid = {C34924A5-C1CA-4D61-87AB-BD36772BA85E} 144 | EndGlobalSection 145 | EndGlobal 146 | -------------------------------------------------------------------------------- /Src/Twilight.CQRS/Commands/CqrsCommandHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Diagnostics; 2 | using Microsoft.Extensions.Logging; 3 | using System.Diagnostics; 4 | using Twilight.CQRS.Interfaces; 5 | using Twilight.CQRS.Messaging.Interfaces; 6 | // ReSharper disable ExplicitCallerInfoArgument as false positive for StartActivity 7 | 8 | namespace Twilight.CQRS.Commands; 9 | 10 | /// 11 | /// 12 | /// Represents the ability to process (handle) commands. A command handler receives a command and brokers a result. 13 | /// A result is either a successful application of the command, or an exception. This class cannot be instantiated. 14 | /// 15 | /// Implements . 16 | /// Implements . 17 | /// 18 | /// The type of the command. 19 | /// The type of the command handler. 20 | /// The message sender. 21 | /// The logger. 22 | /// The command validator. 23 | /// 24 | /// 25 | public abstract class CqrsCommandHandlerBase( 26 | IMessageSender messageSender, 27 | ILogger> logger, 28 | IValidator? validator = null) : CqrsMessageHandler(logger, validator), ICqrsCommandHandler 29 | where TCommand : class, ICqrsCommand 30 | { 31 | /// 32 | /// Gets the message sender. 33 | /// 34 | /// The message sender. 35 | protected IMessageSender MessageSender { get; } = messageSender; 36 | 37 | /// 38 | public async Task Handle(TCommand command, CancellationToken cancellationToken = default) 39 | { 40 | var guardResult = Result.Try(() => Guard.IsNotNull(command)); 41 | if (guardResult.IsFailed) 42 | { 43 | return guardResult; 44 | } 45 | 46 | using var activity = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity($"Handle {command.GetType()}") : null; 47 | 48 | var preHandlingResult = await ExecutePreHandlingAsync(command, cancellationToken); 49 | 50 | if (!preHandlingResult.IsSuccess) 51 | { 52 | return preHandlingResult; 53 | } 54 | 55 | var validationResult = await ExecuteValidationAsync(command, cancellationToken); 56 | 57 | if (!validationResult.IsSuccess) 58 | { 59 | return validationResult; 60 | } 61 | 62 | var handleResult = await ExecuteHandleCommandAsync(command, cancellationToken); 63 | 64 | if (!handleResult.IsSuccess) 65 | { 66 | return handleResult; 67 | } 68 | 69 | var postHandlingResult = await ExecutePostHandlingAsync(command, cancellationToken); 70 | 71 | return postHandlingResult.IsSuccess 72 | ? Result.Ok() 73 | : postHandlingResult; 74 | } 75 | 76 | private static bool ShouldCreateActivity() => Activity.Current?.Source.HasListeners() ?? false; 77 | 78 | /// 79 | /// Handles the command. 80 | /// 81 | /// The command. 82 | /// The cancellation token. 83 | /// A task that represents the asynchronous handle command operation. 84 | public abstract Task HandleCommand(TCommand command, CancellationToken cancellationToken = default); 85 | 86 | private async Task ExecutePreHandlingAsync(TCommand command, CancellationToken cancellationToken) 87 | { 88 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Pre command handling actions") : null; 89 | 90 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,>)}.{nameof(OnBeforeHandling)}")); 91 | 92 | return await OnBeforeHandling(command, cancellationToken); 93 | } 94 | 95 | private async Task ExecuteValidationAsync(TCommand command, CancellationToken cancellationToken) 96 | { 97 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Validate command") : null; 98 | 99 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,>)}.{nameof(ValidateMessage)}")); 100 | 101 | return await ValidateMessage(command, cancellationToken); 102 | } 103 | 104 | private async Task ExecuteHandleCommandAsync(TCommand command, CancellationToken cancellationToken) 105 | { 106 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Handle command") : null; 107 | 108 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,>)}.{nameof(HandleCommand)}")); 109 | 110 | return await HandleCommand(command, cancellationToken); 111 | } 112 | 113 | private async Task ExecutePostHandlingAsync(TCommand command, CancellationToken cancellationToken) 114 | { 115 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Post command handling actions") : null; 116 | 117 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,>)}.{nameof(OnAfterHandling)}")); 118 | 119 | return await OnAfterHandling(command, cancellationToken); 120 | } 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 | /// The message sender. 135 | /// The logger. 136 | /// The command validator. 137 | /// 138 | /// 139 | public abstract class CqrsCommandHandlerBase( 140 | IMessageSender messageSender, 141 | ILogger> logger, 142 | IValidator? validator = null) : CqrsMessageHandler(logger, validator), ICqrsCommandHandler 143 | where TCommand : class, ICqrsCommand 144 | where TResponse : class, ICqrsMessage 145 | { 146 | /// 147 | /// Gets the message sender. 148 | /// 149 | /// The message sender. 150 | protected IMessageSender MessageSender { get; } = messageSender; 151 | 152 | /// 153 | public async Task> Handle(TCommand command, CancellationToken cancellationToken = default) 154 | { 155 | var guardResult = Result.Try(() => Guard.IsNotNull(command)); 156 | 157 | if (guardResult.IsFailed) 158 | { 159 | return guardResult; 160 | } 161 | 162 | using var activity = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity($"Handle {command.GetType()}") : null; 163 | 164 | var preHandlingResult = await ExecutePreHandlingAsync(command, cancellationToken); 165 | 166 | if (!preHandlingResult.IsSuccess) 167 | { 168 | return preHandlingResult; 169 | } 170 | 171 | var validationResult = await ExecuteValidationAsync(command, cancellationToken); 172 | 173 | if (!validationResult.IsSuccess) 174 | { 175 | return validationResult; 176 | } 177 | 178 | var commandResult = await ExecuteHandleCommandAsync(command, cancellationToken); 179 | 180 | if (!commandResult.IsSuccess) 181 | { 182 | return commandResult; 183 | } 184 | 185 | var postHandlingResult = await ExecutePostHandlingAsync(command, cancellationToken); 186 | 187 | return postHandlingResult.IsSuccess 188 | ? commandResult 189 | : postHandlingResult; 190 | } 191 | 192 | private static bool ShouldCreateActivity() => Activity.Current?.Source.HasListeners() ?? false; 193 | 194 | /// 195 | /// Handles the command. 196 | /// 197 | /// The command. 198 | /// The cancellation token. 199 | /// 200 | /// A task that represents the asynchronous command handler operation. 201 | /// The task result contains the command execution response. 202 | /// 203 | protected abstract Task> HandleCommand(TCommand command, CancellationToken cancellationToken = default); 204 | 205 | private async Task> ExecutePreHandlingAsync(TCommand command, CancellationToken cancellationToken) 206 | { 207 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Pre command handling actions") : null; 208 | 209 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,,>)}.{nameof(OnBeforeHandling)}")); 210 | 211 | return await OnBeforeHandling(command, cancellationToken); 212 | } 213 | 214 | private async Task> ExecuteValidationAsync(TCommand command, CancellationToken cancellationToken) 215 | { 216 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Validate command") : null; 217 | 218 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,,>)}.{nameof(ValidateMessage)}")); 219 | 220 | return await ValidateMessage(command, cancellationToken); 221 | } 222 | 223 | private async Task> ExecuteHandleCommandAsync(TCommand command, CancellationToken cancellationToken) 224 | { 225 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Handle command") : null; 226 | 227 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,,>)}.{nameof(HandleCommand)}")); 228 | 229 | return await HandleCommand(command, cancellationToken); 230 | } 231 | 232 | private async Task> ExecutePostHandlingAsync(TCommand command, CancellationToken cancellationToken) 233 | { 234 | using var childSpan = ShouldCreateActivity() ? Activity.Current?.Source.StartActivity("Post command handling actions") : null; 235 | 236 | childSpan?.AddEvent(new ActivityEvent($"{nameof(CqrsCommandHandlerBase<,,>)}.{nameof(OnAfterHandling)}")); 237 | 238 | return await OnAfterHandling(command, cancellationToken); 239 | } 240 | } -------------------------------------------------------------------------------- /Src/Twilight.CQRS.Messaging.InMemory.Autofac/AutofacInMemoryMessageSender.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Autofac.Core; 3 | using Autofac.Core.Registration; 4 | using CommunityToolkit.Diagnostics; 5 | using Microsoft.Extensions.Logging; 6 | using System.Collections.Concurrent; 7 | using System.Diagnostics; 8 | using System.Reflection; 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( 24 | ILifetimeScope lifetimeScope, 25 | ILogger logger) : IMessageSender 26 | { 27 | private const string DefaultAssemblyVersion = "1.0.0.0"; 28 | 29 | private static string Namespace => typeof(AutofacInMemoryMessageSender).Namespace ?? nameof(AutofacInMemoryMessageSender); 30 | 31 | private static string AssemblyVersion => Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? DefaultAssemblyVersion; 32 | 33 | private static readonly ActivitySource ActivitySource = new(Namespace, AssemblyVersion); 34 | 35 | // Cache for reflection MethodInfo objects to avoid repeated lookups 36 | private static readonly ConcurrentDictionary<(Type HandlerType, Type MessageType), MethodInfo> MethodCache = new(); 37 | 38 | // Cache for closed generic types to avoid repeated MakeGenericType calls 39 | private static readonly ConcurrentDictionary<(Type GenericType, Type MessageType, Type? ResultType), Type> TypeCache = new(); 40 | 41 | /// 42 | public async Task Send(TCommand command, CancellationToken cancellationToken = default) 43 | where TCommand : class, ICqrsCommand 44 | { 45 | var guardResult = ValidateNotNull(command); 46 | 47 | if (guardResult.IsFailed) 48 | { 49 | return guardResult; 50 | } 51 | 52 | using var activity = CreateActivity($"Send {command.GetType()}"); 53 | 54 | var assemblyQualifiedName = typeof(ICqrsCommandHandler).AssemblyQualifiedName ?? "Unknown Assembly"; 55 | 56 | var handlersResult = TryResolveCommandHandlers(assemblyQualifiedName); 57 | 58 | if (handlersResult.IsFailed) 59 | { 60 | return Result.Fail(handlersResult.Errors); // Convert Result to Result 61 | } 62 | 63 | var commandHandlers = handlersResult.Value.ToList(); 64 | 65 | var validationResult = ValidateCommandHandlerCount(commandHandlers.Count, assemblyQualifiedName); 66 | 67 | if (validationResult.IsFailed) 68 | { 69 | return validationResult; 70 | } 71 | 72 | await commandHandlers[0].Handle(command, cancellationToken); 73 | 74 | return Result.Ok(); 75 | } 76 | 77 | /// 78 | public async Task> Send(ICqrsCommand command, CancellationToken cancellationToken = default) 79 | { 80 | var guardResult = ValidateNotNull(command); 81 | 82 | if (guardResult.IsFailed) 83 | { 84 | return guardResult; 85 | } 86 | 87 | var commandType = command.GetType(); 88 | var genericType = typeof(ICqrsCommandHandler<,>); 89 | 90 | var closedGenericType = TypeCache.GetOrAdd((genericType, commandType, typeof(TResult)), key => 91 | key.GenericType.MakeGenericType(key.MessageType, key.ResultType!)); 92 | 93 | var assemblyQualifiedName = closedGenericType.AssemblyQualifiedName ?? "Unknown Assembly"; 94 | 95 | using var activity = CreateActivity($"Send {commandType}"); 96 | 97 | return await ExecuteHandlerWithResult(closedGenericType, command, assemblyQualifiedName, cancellationToken); 98 | } 99 | 100 | /// 101 | public async Task> Send(ICqrsQuery query, CancellationToken cancellationToken = default) 102 | { 103 | var guardResult = ValidateNotNull(query); 104 | 105 | if (guardResult.IsFailed) 106 | { 107 | return guardResult; 108 | } 109 | 110 | var queryType = query.GetType(); 111 | var genericType = typeof(ICqrsQueryHandler<,>); 112 | 113 | var closedGenericType = TypeCache.GetOrAdd((genericType, queryType, typeof(TResult)), key => 114 | key.GenericType.MakeGenericType(key.MessageType, key.ResultType!)); 115 | 116 | var assemblyQualifiedName = closedGenericType.AssemblyQualifiedName ?? "Unknown Assembly"; 117 | 118 | using var activity = CreateActivity($"Send {queryType}"); 119 | 120 | return await ExecuteHandlerWithResult(closedGenericType, query, assemblyQualifiedName, cancellationToken); 121 | } 122 | 123 | /// 124 | public async Task Publish(IEnumerable events, CancellationToken cancellationToken = default) 125 | where TEvent : class, ICqrsEvent 126 | { 127 | TEvent[] eventsList = [.. events]; 128 | 129 | if (eventsList.Length == 0) 130 | { 131 | logger.LogNoEventsReceived(); 132 | } 133 | 134 | foreach (var @event in eventsList) 135 | { 136 | await Publish(@event, cancellationToken); 137 | } 138 | 139 | return Result.Ok(); 140 | } 141 | 142 | /// 143 | public async Task Publish(TEvent @event, CancellationToken cancellationToken = default) 144 | where TEvent : class, ICqrsEvent 145 | { 146 | var guardResult = ValidateNotNull(@event); 147 | 148 | if (guardResult.IsFailed) 149 | { 150 | return guardResult; 151 | } 152 | 153 | using var activity = CreateActivity($"Publish {@event.GetType()}"); 154 | 155 | var assemblyQualifiedName = typeof(ICqrsEventHandler).AssemblyQualifiedName ?? "Unknown Assembly"; 156 | 157 | var handlersResult = TryResolveEventHandlers(assemblyQualifiedName); 158 | 159 | if (handlersResult.IsFailed) 160 | { 161 | return Result.Fail(handlersResult.Errors); // Convert Result to Result 162 | } 163 | 164 | var handlers = handlersResult.Value; 165 | 166 | if (handlers.Count == 0) 167 | { 168 | LogHandlerNotFound(assemblyQualifiedName); 169 | return Result.Fail("No handler could be found for this request."); 170 | } 171 | 172 | var tasks = handlers.Select(handler => handler.Handle(@event, cancellationToken)); 173 | 174 | await Task.WhenAll(tasks); 175 | 176 | return Result.Ok(); 177 | } 178 | 179 | private static Result ValidateNotNull(object? obj) 180 | => Result.Try(() => Guard.IsNotNull(obj)); 181 | 182 | private static Activity? CreateActivity(string activityName) 183 | => ActivitySource.StartActivity(activityName); 184 | 185 | private Result>> TryResolveCommandHandlers(string assemblyQualifiedName) 186 | where TCommand : class, ICqrsCommand 187 | { 188 | try 189 | { 190 | lifetimeScope.TryResolve(out IEnumerable>? handlers); 191 | 192 | return Result.Ok(handlers ?? []); 193 | } 194 | catch (DependencyResolutionException ex) 195 | { 196 | LogDependencyResolutionError(ex, assemblyQualifiedName); 197 | 198 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 199 | } 200 | } 201 | 202 | private Result>> TryResolveEventHandlers(string assemblyQualifiedName) 203 | where TEvent : class, ICqrsEvent 204 | { 205 | try 206 | { 207 | var handlers = lifetimeScope.Resolve>>().ToList(); 208 | 209 | return Result.Ok>>(handlers); 210 | } 211 | catch (ComponentNotRegisteredException ex) 212 | { 213 | LogComponentNotRegistered(ex, assemblyQualifiedName); 214 | 215 | return Result.Fail("A component is not registered in the DI container. Check your component is registered."); 216 | } 217 | catch (DependencyResolutionException ex) 218 | { 219 | LogDependencyResolutionError(ex, assemblyQualifiedName); 220 | 221 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 222 | } 223 | } 224 | 225 | private Result ValidateCommandHandlerCount(int handlerCount, string assemblyQualifiedName) 226 | => handlerCount switch 227 | { 228 | 0 => HandleNoHandlerFound(assemblyQualifiedName), 229 | > 1 => HandleMultipleHandlersFound(assemblyQualifiedName), 230 | _ => Result.Ok() 231 | }; 232 | 233 | private Result HandleNoHandlerFound(string assemblyQualifiedName) 234 | { 235 | LogHandlerNotFound(assemblyQualifiedName); 236 | 237 | return Result.Fail("No handler could be found for this request."); 238 | } 239 | 240 | private Result HandleMultipleHandlersFound(string assemblyQualifiedName) 241 | { 242 | logger.LogMultipleHandlersFound(assemblyQualifiedName); 243 | 244 | return Result.Fail("Multiple handlers found. A command may only have one handler."); 245 | } 246 | 247 | private async Task> ExecuteHandlerWithResult(Type closedGenericType, object message, string assemblyQualifiedName, CancellationToken cancellationToken) 248 | { 249 | try 250 | { 251 | var handlerExists = lifetimeScope.TryResolve(closedGenericType, out var handler); 252 | 253 | if (!handlerExists) 254 | { 255 | LogHandlerNotFound(assemblyQualifiedName); 256 | return Result.Fail("No handler could be found for this request."); 257 | } 258 | 259 | var result = await InvokeHandler(handler!, message, cancellationToken); 260 | 261 | return result; 262 | } 263 | catch (DependencyResolutionException ex) 264 | { 265 | LogDependencyResolutionError(ex, assemblyQualifiedName); 266 | 267 | return Result.Fail("Failed to resolve dependency from the DI container. Check your component is registered."); 268 | } 269 | catch (InvalidOperationException ex) 270 | { 271 | LogHandlerExecutionError(ex, assemblyQualifiedName); 272 | 273 | return Result.Fail("Failed to execute handler."); 274 | } 275 | } 276 | 277 | private static async Task> InvokeHandler(object handler, object message, CancellationToken cancellationToken) 278 | { 279 | var handlerType = handler.GetType(); 280 | var messageType = message.GetType(); 281 | 282 | var method = MethodCache.GetOrAdd((handlerType, messageType), key 283 | => key.HandlerType.GetRuntimeMethod("Handle", [key.MessageType, typeof(CancellationToken)]) 284 | ?? throw new InvalidOperationException($"Failed to get runtime method 'Handle' from {key.HandlerType}.")); 285 | 286 | var resultTask = (Task>)method.Invoke(handler, [message, cancellationToken])!; 287 | 288 | return await resultTask; 289 | } 290 | 291 | private void LogDependencyResolutionError(Exception ex, string assemblyQualifiedName) 292 | => logger.LogHandlerResolutionFailure(assemblyQualifiedName, ex); 293 | 294 | private void LogComponentNotRegistered(Exception ex, string assemblyQualifiedName) 295 | => logger.LogComponentNotFound(assemblyQualifiedName, ex); 296 | 297 | private void LogHandlerNotFound(string assemblyQualifiedName) 298 | => logger.LogHandlerNotFoundCritical(assemblyQualifiedName); 299 | 300 | private void LogHandlerExecutionError(Exception ex, string assemblyQualifiedName) 301 | => logger.LogHandlerExecutionFailure(assemblyQualifiedName, ex); 302 | } 303 | --------------------------------------------------------------------------------