From dcf46f54020ed5b7b649415fad197a13781b01cd Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Wed, 25 Mar 2026 20:18:31 +0000 Subject: [PATCH 1/4] Enhance mediator functionality with manual handler registration and configuration options - Introduced `AddHandler()` method for manual registration of command, query, and notification handlers. - Updated documentation to reflect changes in handler registration and notification publish modes. - Added `MediatorConfiguration`, `MediatorHandlerConfiguration`, and `MediatorHandlerKind` classes for improved configuration management. - Implemented `MediatorHandlerDescriptor` for fluent API in handler configuration. - Added tests for manual handler registration and dispatching various command, query, and notification types. - Improved error handling for invalid handler types during registration. --- src/Mocha/Mocha.slnx | 2 + src/Mocha/benchmarks/Directory.Packages.props | 3 + .../Mocha.Mediator.Benchmarks/Program.cs | 2 +- .../DependencyInjectionFileBuilder.cs | 132 ++++-------- .../DependencyInjectionGenerator.cs | 48 ++--- .../Inspectors/HandlerInspector.cs | 2 +- .../Inspectors/MessageTypeInspector.cs | 2 +- .../src/Mocha.Analyzers/Models/HandlerKind.cs | 2 +- .../src/Mocha.Analyzers/Models/MessageKind.cs | 2 +- .../INotificationStrategy.cs | 24 --- .../Configuration/MediatorConfiguration.cs | 24 +++ .../MediatorHandlerConfiguration.cs | 37 ++++ .../Configuration/MediatorHandlerKind.cs | 27 +++ .../DependencyInjection/IMediatorBuilder.cs | 11 +- .../DependencyInjection/MediatorBuilder.cs | 158 ++++++++++++-- .../MediatorHostBuilderHandlerExtensions.cs | 61 ++++++ .../IHasMediatorConfigurationContext.cs | 12 ++ .../IMediatorConfigurationContext.cs | 17 ++ .../Descriptors/IMediatorDescriptor.cs | 34 +++ .../IMediatorDescriptorExtension.cs | 26 +++ .../Descriptors/IMediatorHandlerDescriptor.cs | 6 + .../MediatorConfigurationContext.cs | 13 ++ .../Descriptors/MediatorDescriptorBase.cs | 39 ++++ .../Descriptors/MediatorHandlerDescriptor.cs | 91 ++++++++ src/Mocha/src/Mocha.Mediator/Mediator.cs | 112 ++++++++-- .../src/Mocha.Mediator/MediatorOptions.cs | 29 +++ .../src/Mocha.Mediator/MediatorRuntime.cs | 32 ++- .../Pipeline/ForeachAwaitPublisher.cs | 50 ----- .../Pipeline/MediatorPipelineConfiguration.cs | 31 --- .../Pipeline/NotificationStrategyFeature.cs | 9 - .../Pipeline/PipelineBuilder.cs | 78 ++----- .../Pipeline/TaskWhenAllPublisher.cs | 54 ----- .../Mocha/Descriptors/IMessagingDescriptor.cs | 8 +- ...on.cs => IMessagingDescriptorExtension.cs} | 6 +- .../Descriptors/MessagingDescriptorBase.cs | 12 +- .../Transport/MessagingTransportDescriptor.cs | 16 +- ...mandWithResponseHandler_MatchesSnapshot.md | 17 +- ...MultipleCommandHandlers_MatchesSnapshot.md | 31 ++- ...rate_VoidCommandHandler_MatchesSnapshot.md | 17 +- ...002_CommandWithTwoHandlers_ReportsError.md | 25 +-- ...VoidCommandWithTwoHandlers_ReportsError.md | 25 +-- ...s.MO0004_OpenGenericCommand_ReportsInfo.md | 17 +- ...sts.MO0004_OpenGenericQuery_ReportsInfo.md | 17 +- ...Warning_CommandWithHandler_NoDiagnostic.md | 17 +- ...rate_ModuleWithOnlyName_MatchesSnapshot.md | 17 +- ...pace_DeterministicOrder_MatchesSnapshot.md | 33 ++- ...rate_OpenGenericCommand_MatchesSnapshot.md | 17 +- ...enerate_InternalHandler_MatchesSnapshot.md | 17 +- ...olution_ICommandGeneric_MatchesSnapshot.md | 17 +- ...ution_ICommandInterface_MatchesSnapshot.md | 17 +- ...ypes_AllSymbolsResolved_MatchesSnapshot.md | 49 +++-- ...ultAssemblyName_PrefixesWithLastSegment.md | 17 +- ...rate_DottedAssemblyName_UsesLastSegment.md | 17 +- ...ModuleFile_ContainsHandlerRegistrations.md | 17 +- ...enerate_AllHandlerTypes_MatchesSnapshot.md | 58 ++--- ...rsInDifferentNamespaces_MatchesSnapshot.md | 25 +-- ...sesLastSegmentSanitized_MatchesSnapshot.md | 17 +- ...ame_UsesAssemblyDefault_MatchesSnapshot.md | 17 +- ...rate_NestedClassHandler_MatchesSnapshot.md | 17 +- ...lersForSameNotification_MatchesSnapshot.md | 26 +-- ...ngleNotificationHandler_MatchesSnapshot.md | 17 +- ...ate_PartialClassHandler_MatchesSnapshot.md | 17 +- ...ueryHandler_AcrossFiles_MatchesSnapshot.md | 17 +- ...mandHandler_AcrossFiles_MatchesSnapshot.md | 17 +- ...e_MultipleQueryHandlers_MatchesSnapshot.md | 25 +-- ...s.Generate_QueryHandler_MatchesSnapshot.md | 17 +- ...hod_WithAllHandlerTypes_MatchesSnapshot.md | 49 +++-- ...tityFrameworkTransactionMiddlewareTests.cs | 44 +--- .../Mocha.Mediator.Tests/AddHandlerTests.cs | 198 ++++++++++++++++++ .../ContextPoolingTests.cs | 41 +--- .../InstrumentationTests.cs | 30 +-- .../MiddlewareFactoryContextTests.cs | 69 ++---- .../MiddlewarePipelineTests.cs | 64 ++---- .../NamedMediatorTests.cs | 27 +-- .../NotificationStrategyTests.cs | 57 +++-- .../test/Mocha.Mediator.Tests/TestSetup.cs | 187 ++++++----------- website/src/docs/mocha/v1/mediator/index.md | 50 ++++- .../v1/mediator/pipeline-and-middleware.md | 136 +++++++----- 78 files changed, 1561 insertions(+), 1257 deletions(-) delete mode 100644 src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Configuration/MediatorConfiguration.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerKind.cs create mode 100644 src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/IHasMediatorConfigurationContext.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorConfigurationContext.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptorExtension.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorHandlerDescriptor.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/MediatorConfigurationContext.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs create mode 100644 src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs delete mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs delete mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs delete mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs delete mode 100644 src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs rename src/Mocha/src/Mocha/Descriptors/{IDescriptorExtension.cs => IMessagingDescriptorExtension.cs} (66%) create mode 100644 src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs diff --git a/src/Mocha/Mocha.slnx b/src/Mocha/Mocha.slnx index 76b017298ef..95383168cee 100644 --- a/src/Mocha/Mocha.slnx +++ b/src/Mocha/Mocha.slnx @@ -16,6 +16,8 @@ + + diff --git a/src/Mocha/benchmarks/Directory.Packages.props b/src/Mocha/benchmarks/Directory.Packages.props index 163a961330c..6ced7aee888 100644 --- a/src/Mocha/benchmarks/Directory.Packages.props +++ b/src/Mocha/benchmarks/Directory.Packages.props @@ -2,6 +2,9 @@ true + + + diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs index 98103c9af46..0ba0e169aec 100644 --- a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Program.cs @@ -7,6 +7,6 @@ var config = DefaultConfig.Instance .AddJob(Job.Default.WithToolchain( CsProjCoreToolchain.From( - new NetCoreAppSettings("net9.0", null, ".NET 9.0")))); + new NetCoreAppSettings("net10.0", null, ".NET 10.0")))); BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); diff --git a/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs b/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs index 2cbe36e7c98..f8e520c70e2 100644 --- a/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs +++ b/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs @@ -42,102 +42,65 @@ public void WriteBeginRegistrationMethod() Writer.DecreaseIndent(); Writer.WriteIndentedLine("{"); Writer.IncreaseIndent(); - - Writer.WriteIndentedLine("var services = builder.Services;"); - Writer.WriteIndentedLine("var lifetime = builder.Options.ServiceLifetime;"); - } - - public void WriteHandlerRegistration(HandlerInfo handler) - { - switch (handler.Kind) - { - case HandlerKind.CommandVoid: - WriteServiceDescriptor( - "global::Mocha.Mediator.ICommandHandler<{0}>", - handler.HandlerTypeName, - handler.MessageTypeName); - break; - case HandlerKind.CommandResponse: - WriteServiceDescriptor( - "global::Mocha.Mediator.ICommandHandler<{0}, {1}>", - handler.HandlerTypeName, - handler.MessageTypeName, - handler.ResponseTypeName!); - break; - case HandlerKind.Query: - WriteServiceDescriptor( - "global::Mocha.Mediator.IQueryHandler<{0}, {1}>", - handler.HandlerTypeName, - handler.MessageTypeName, - handler.ResponseTypeName!); - break; - } - } - - public void WriteNotificationHandlerRegistration(NotificationHandlerInfo handler) - { - // Use TryAddEnumerable to prevent duplicate handler registrations - // when the generated Add{Module} extension is called more than once - // (e.g. in tests or modular startup). - Writer.WriteIndentedLine( - "global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler<{0}>), typeof({1}), lifetime));", - handler.NotificationTypeName, - handler.HandlerTypeName); - } - - /// - /// Writes the opening of a ConfigureMediator lambda for deferred pipeline registrations. - /// - public void WriteBeginConfigureMediator() - { - Writer.WriteIndentedLine("global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b =>"); - Writer.WriteIndentedLine("{"); - Writer.IncreaseIndent(); - } - - /// - /// Writes the closing of a ConfigureMediator lambda. - /// - public void WriteEndConfigureMediator() - { - Writer.DecreaseIndent(); - Writer.WriteIndentedLine("});"); } /// - /// Writes a pipeline registration for a handler (inside ConfigureMediator lambda, using 'b'). + /// Writes an AddHandlerConfiguration call for a command/query handler. /// - public void WritePipelineRegistration(HandlerInfo handler) + public void WriteHandlerConfiguration(HandlerInfo handler) { - var (terminalMethod, responseType) = handler.Kind switch + var (kindEnum, terminalCall) = handler.Kind switch { - HandlerKind.CommandVoid => ($"BuildVoidCommandTerminal<{handler.MessageTypeName}>()", null), - HandlerKind.CommandResponse => ($"BuildCommandTerminal<{handler.MessageTypeName}, {handler.ResponseTypeName}>()", handler.ResponseTypeName), - HandlerKind.Query => ($"BuildQueryTerminal<{handler.MessageTypeName}, {handler.ResponseTypeName}>()", handler.ResponseTypeName), + HandlerKind.Command => ( + "Command", + $"BuildCommandPipeline<{handler.HandlerTypeName}, {handler.MessageTypeName}>()"), + HandlerKind.CommandResponse => ( + "CommandResponse", + $"BuildCommandResponsePipeline<{handler.HandlerTypeName}, {handler.MessageTypeName}, {handler.ResponseTypeName}>()"), + HandlerKind.Query => ( + "Query", + $"BuildQueryPipeline<{handler.HandlerTypeName}, {handler.MessageTypeName}, {handler.ResponseTypeName}>()"), _ => throw new ArgumentOutOfRangeException() }; - WritePipelineConfiguration(handler.MessageTypeName, responseType, terminalMethod); + WriteAddHandlerConfiguration( + handler.HandlerTypeName, + handler.MessageTypeName, + handler.ResponseTypeName, + kindEnum, + $"global::Mocha.Mediator.PipelineBuilder.{terminalCall}"); } /// - /// Writes a pipeline registration for a notification group (inside ConfigureMediator lambda, using 'b'). + /// Writes an AddHandlerConfiguration call for a notification handler. /// - public void WriteNotificationPipelineRegistration(string notificationType, - List groupHandlers) + public void WriteNotificationHandlerConfiguration( + string notificationType, + NotificationHandlerInfo handler) { - var handlerTypeArgs = string.Join(", ", - groupHandlers.Select(h => $"typeof({h.HandlerTypeName})")); - - var terminalMethod = $"BuildNotificationTerminal<{notificationType}>(new global::System.Type[] {{ {handlerTypeArgs} }})"; - WritePipelineConfiguration(notificationType, null, terminalMethod); + WriteAddHandlerConfiguration( + handler.HandlerTypeName, + notificationType, + responseTypeName: null, + "Notification", + $"global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline<{handler.HandlerTypeName}, {notificationType}>()"); } - private void WritePipelineConfiguration(string messageTypeName, string? responseTypeName, string terminalMethod) + private void WriteAddHandlerConfiguration( + string handlerTypeName, + string messageTypeName, + string? responseTypeName, + string kindEnum, + string terminalCall) { - Writer.WriteIndentedLine("b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration"); + Writer.WriteIndentedLine( + "global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration<{0}>(builder,", + handlerTypeName); + Writer.IncreaseIndent(); + Writer.WriteIndentedLine("new global::Mocha.Mediator.MediatorHandlerConfiguration"); Writer.WriteIndentedLine("{"); Writer.IncreaseIndent(); + Writer.WriteIndentedLine("HandlerType = typeof({0}),", handlerTypeName); Writer.WriteIndentedLine("MessageType = typeof({0}),", messageTypeName); if (responseTypeName is not null) @@ -145,9 +108,11 @@ private void WritePipelineConfiguration(string messageTypeName, string? response Writer.WriteIndentedLine("ResponseType = typeof({0}),", responseTypeName); } - Writer.WriteIndentedLine("Terminal = global::Mocha.Mediator.PipelineBuilder.{0}", terminalMethod); + Writer.WriteIndentedLine("Kind = global::Mocha.Mediator.MediatorHandlerKind.{0},", kindEnum); + Writer.WriteIndentedLine("Delegate = {0}", terminalCall); Writer.DecreaseIndent(); Writer.WriteIndentedLine("});"); + Writer.DecreaseIndent(); } public void WriteSectionComment(string comment) @@ -164,17 +129,6 @@ public void WriteEndRegistrationMethod() Writer.WriteIndentedLine("}"); } - private void WriteServiceDescriptor(string serviceTypeFormat, string implementationType, params object[] typeArgs) - { - // Use TryAdd to prevent duplicate handler registrations - // when the generated Add{Module} extension is called more than once. - var serviceType = string.Format(serviceTypeFormat, typeArgs); - Writer.WriteIndentedLine( - "global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof({0}), typeof({1}), lifetime));", - serviceType, - implementationType); - } - #pragma warning disable CA5351 // MD5 is used for non-security hashing (file name salting) private static readonly MD5 s_md5 = MD5.Create(); #pragma warning restore CA5351 diff --git a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs index 53e2193b727..197cff1c0c9 100644 --- a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs +++ b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs @@ -37,57 +37,33 @@ public void Generate( using var builder = new DependencyInjectionFileBuilder(moduleName, assemblyName); - builder.WriteHeader(); - builder.WriteBeginNamespace(); - builder.WriteBeginClass(); - builder.WriteBeginRegistrationMethod(); - - // Register handlers - if (handlers.Count > 0) - { - builder.WriteSectionComment("Register handlers"); - - foreach (var handler in handlers) - { - builder.WriteHandlerRegistration(handler); - } - } - - // Register notification handlers - if (notificationHandlers.Count > 0) - { - builder.WriteSectionComment("Register notification handlers"); - - foreach (var handler in notificationHandlers) - { - builder.WriteNotificationHandlerRegistration(handler); - } - } - - // Register pipelines (all handlers + notifications) via deferred ConfigureMediator var notificationGroups = notificationHandlers .GroupBy(h => h.NotificationTypeName) .OrderBy(g => g.Key) .ToList(); + builder.WriteHeader(); + builder.WriteBeginNamespace(); + builder.WriteBeginClass(); + builder.WriteBeginRegistrationMethod(); + + // Register all handler configurations if (handlers.Count > 0 || notificationGroups.Count > 0) { - builder.WriteSectionComment("Register pipelines"); - builder.WriteBeginConfigureMediator(); + builder.WriteSectionComment("Register handler configurations"); foreach (var handler in handlers) { - builder.WritePipelineRegistration(handler); + builder.WriteHandlerConfiguration(handler); } foreach (var group in notificationGroups) { - builder.WriteNotificationPipelineRegistration( - group.Key, - group.OrderBy(h => h.HandlerTypeName).ToList()); + foreach (var handler in group.OrderBy(h => h.HandlerTypeName)) + { + builder.WriteNotificationHandlerConfiguration(group.Key, handler); + } } - - builder.WriteEndConfigureMediator(); } builder.WriteEndRegistrationMethod(); diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs index d3989d8397b..9e6d933551b 100644 --- a/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs @@ -23,7 +23,7 @@ public sealed class HandlerInspector : ISyntaxInspector private static readonly HandlerKindDescriptor[] s_handlerKinds = [ - new(static s => s.ICommandHandlerVoid, HandlerKind.CommandVoid, HasResponse: false), + new(static s => s.ICommandHandlerVoid, HandlerKind.Command, HasResponse: false), new(static s => s.ICommandHandlerResponse, HandlerKind.CommandResponse, HasResponse: true), new(static s => s.IQueryHandler, HandlerKind.Query, HasResponse: true) ]; diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs index 19e5ad0bb35..b2ec4e3e74f 100644 --- a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessageTypeInspector.cs @@ -98,7 +98,7 @@ public bool TryHandle( return true; } - syntaxInfo = new MessageTypeInfo(typeName, typeNamespace, MessageKind.CommandVoid, location); + syntaxInfo = new MessageTypeInfo(typeName, typeNamespace, MessageKind.Command, location); return true; } diff --git a/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs b/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs index 8093c8e1b56..525fefcc8f1 100644 --- a/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs +++ b/src/Mocha/src/Mocha.Analyzers/Models/HandlerKind.cs @@ -8,7 +8,7 @@ public enum HandlerKind /// /// A command handler that returns no response. /// - CommandVoid, + Command, /// /// A command handler that returns a response. diff --git a/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs b/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs index ed34c42613e..69435715d4d 100644 --- a/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs +++ b/src/Mocha/src/Mocha.Analyzers/Models/MessageKind.cs @@ -8,7 +8,7 @@ public enum MessageKind /// /// A void command (no response). /// - CommandVoid, + Command, /// /// A command that returns a response. diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs deleted file mode 100644 index c14decf82ea..00000000000 --- a/src/Mocha/src/Mocha.Mediator.Abstractions/INotificationStrategy.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Mocha.Mediator; - -/// -/// Defines the strategy for dispatching notifications to multiple handlers. -/// -/// -/// Implementations control how handlers are invoked (e.g., sequentially, in parallel, or with specific error handling). -/// -public interface INotificationStrategy -{ - /// - /// Publishes a notification to all of the provided handlers using this strategy. - /// - /// The type of notification to publish. - /// The collection of handlers to dispatch the notification to. - /// The notification to publish. - /// A token that may be used to cancel the asynchronous operation. - /// A representing the asynchronous operation. - ValueTask PublishAsync( - IReadOnlyList> handlers, - TNotification notification, - CancellationToken cancellationToken) - where TNotification : INotification; -} diff --git a/src/Mocha/src/Mocha.Mediator/Configuration/MediatorConfiguration.cs b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorConfiguration.cs new file mode 100644 index 00000000000..d42c2ed0cbb --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorConfiguration.cs @@ -0,0 +1,24 @@ +using Mocha.Features; + +namespace Mocha.Mediator; + +/// +/// Base class for mediator configuration objects that support feature-based extensibility through +/// . +/// +public abstract class MediatorConfiguration : IFeatureProvider +{ + private IFeatureCollection? _features; + + /// + /// Get access to context data that are copied to the type + /// and can be used for customizations. + /// + public virtual IFeatureCollection Features => _features ??= new FeatureCollection(); + + /// + /// Get access to features that are copied to the type + /// and can be used for customizations. + /// + public IFeatureCollection GetFeatures() => _features ?? FeatureCollection.Empty; +} diff --git a/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs new file mode 100644 index 00000000000..430e626060c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs @@ -0,0 +1,37 @@ +namespace Mocha.Mediator; + +/// +/// Holds the resolved configuration for a mediator handler, including its type metadata, +/// handler kind, and an optional pre-built terminal delegate. +/// +public class MediatorHandlerConfiguration : MediatorConfiguration +{ + /// + /// Gets or sets the concrete handler implementation type. + /// + public Type? HandlerType { get; set; } + + /// + /// Gets or sets the message type (command, query, or notification type). + /// + public Type? MessageType { get; set; } + + /// + /// Gets or sets the response type, or null for void commands and notifications. + /// + public Type? ResponseType { get; set; } + + /// + /// Gets or sets the kind of handler. + /// + public MediatorHandlerKind Kind { get; set; } + + /// + /// Gets or sets an optional pre-built delegate. When set, the builder uses this + /// directly (AOT-safe, source-generator path). When null, the builder creates the delegate + /// via reflection (manual AddHandler path). + /// This is the innermost delegate that resolves and invokes the handler. + /// It is wrapped in middleware during pipeline compilation. + /// + public MediatorDelegate? Delegate { get; set; } +} diff --git a/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerKind.cs b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerKind.cs new file mode 100644 index 00000000000..5ffa7ae60ab --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerKind.cs @@ -0,0 +1,27 @@ +namespace Mocha.Mediator; + +/// +/// Specifies the kind of mediator handler. +/// +public enum MediatorHandlerKind +{ + /// + /// A command handler that does not return a response. + /// + Command, + + /// + /// A command handler that returns a response. + /// + CommandResponse, + + /// + /// A query handler that returns a response. + /// + Query, + + /// + /// A notification handler. Multiple handlers can be registered for the same notification type. + /// + Notification +} diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs index 81a71c7f94d..5d09f817bf9 100644 --- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/IMediatorBuilder.cs @@ -41,9 +41,16 @@ public interface IMediatorBuilder IMediatorBuilder ConfigureServices(Action configure); /// - /// Registers a pipeline configuration for the specified message type. + /// Registers a handler via descriptor-based configuration. + /// + /// The handler implementation type. + /// An optional action to configure the handler descriptor. + void AddHandler(Action? configure = null) where THandler : class; + + /// + /// Registers a handler with a pre-built configuration. /// This method is intended for use by source-generated code. /// [EditorBrowsable(EditorBrowsableState.Never)] - void RegisterPipeline(MediatorPipelineConfiguration configuration); + void AddHandlerConfiguration(MediatorHandlerConfiguration configuration); } diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs index fbe7e73f89b..c9fe5781270 100644 --- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs @@ -1,4 +1,6 @@ using System.Collections.Frozen; +using System.ComponentModel; +using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Mocha.Features; @@ -12,13 +14,25 @@ namespace Mocha.Mediator; /// public sealed class MediatorBuilder : IMediatorBuilder { + private static readonly MethodInfo s_buildCommandPipeline = + typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildCommandPipeline))!; + + private static readonly MethodInfo s_buildCommandResponsePipeline = + typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildCommandResponsePipeline))!; + + private static readonly MethodInfo s_buildQueryPipeline = + typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildQueryPipeline))!; + + private static readonly MethodInfo s_buildNotificationPipeline = + typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildNotificationPipeline))!; + private readonly List _middlewares = [ MediatorDiagnosticMiddleware.Create() ]; private readonly List>> _pipelineModifiers = []; - private readonly List _pipelines = []; + private readonly Dictionary> _handlerDescriptors = []; private readonly List> _configureServices = []; private readonly List> _configureFeatures = []; private readonly MediatorOptions _options = new(); @@ -93,11 +107,55 @@ public IMediatorBuilder ConfigureServices(Action - public void RegisterPipeline(MediatorPipelineConfiguration configuration) + public void AddHandler(Action? configure = null) + where THandler : class + { + var handlerType = typeof(THandler); + var existing = _handlerDescriptors.GetValueOrDefault(handlerType); + + if (existing is not null && configure is not null) + { + var inner = existing; + _handlerDescriptors[handlerType] = d => { inner(d); configure(d); }; + } + else if (configure is not null) + { + _handlerDescriptors[handlerType] = configure; + } + else + { + _handlerDescriptors.TryAdd(handlerType, static _ => { }); + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void AddHandlerConfiguration(MediatorHandlerConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration); - _pipelines.Add(configuration); + var handlerType = configuration.HandlerType!; + + void ApplyConfig(MediatorHandlerDescriptor d) + { + var config = d.Extend().Configuration; + config.MessageType = configuration.MessageType; + config.ResponseType = configuration.ResponseType; + config.Kind = configuration.Kind; + config.Delegate = configuration.Delegate; + } + + var existing = _handlerDescriptors.GetValueOrDefault(handlerType); + + if (existing is not null) + { + var inner = existing; + _handlerDescriptors[handlerType] = d => { inner(d); ApplyConfig(d); }; + } + else + { + _handlerDescriptors[handlerType] = ApplyConfig; + } } /// @@ -131,12 +189,8 @@ public MediatorRuntime Build(IServiceProvider applicationServices) configure(features); } - if (!features.TryGet(out _)) - { - var strategy = applicationServices.GetService() - ?? new ForeachAwaitPublisher(); - features.Set(new NotificationStrategyFeature(strategy)); - } + // Create the configuration context for descriptor construction. + var configContext = new MediatorConfigurationContext(_options, internalProvider, features); // Compile pipelines using the internal provider for middleware factory context. var factoryCtx = new MediatorMiddlewareFactoryContext { Services = internalProvider, Features = features }; @@ -149,20 +203,92 @@ public MediatorRuntime Build(IServiceProvider applicationServices) ? new IReadOnlyList>>[] { _pipelineModifiers } : []; - var pipelines = new Dictionary(_pipelines.Count); + var pipelines = new Dictionary(); + var notificationTerminals = new Dictionary>(); + + foreach (var (handlerType, configureDelegate) in _handlerDescriptors) + { + var descriptor = new MediatorHandlerDescriptor(configContext, handlerType); + configureDelegate(descriptor); + var config = descriptor.CreateConfiguration(); + + var terminal = config.Delegate ?? BuildPipelineViaReflection(config); + + if (config.Kind == MediatorHandlerKind.Notification) + { + if (!notificationTerminals.TryGetValue(config.MessageType!, out var list)) + { + list = []; + notificationTerminals[config.MessageType!] = list; + } + + list.Add(terminal); + } + else + { + factoryCtx.MessageType = config.MessageType!; + factoryCtx.ResponseType = config.ResponseType; + + pipelines[config.MessageType!] = + MediatorMiddlewareCompiler.Compile(factoryCtx, terminal, middlewareConfigs, modifiers); + } + } + + // Compile notification pipelines - each handler terminal is independently + // wrapped in middleware, producing a MediatorDelegate[] per notification type. + var notificationPipelines = new Dictionary(notificationTerminals.Count); - foreach (var config in _pipelines) + foreach (var (notificationType, terminals) in notificationTerminals) { - factoryCtx.MessageType = config.MessageType; - factoryCtx.ResponseType = config.ResponseType; + factoryCtx.MessageType = notificationType; + factoryCtx.ResponseType = null; - pipelines[config.MessageType] = - MediatorMiddlewareCompiler.Compile(factoryCtx, config.Terminal, middlewareConfigs, modifiers); + var compiled = new MediatorDelegate[terminals.Count]; + for (var i = 0; i < terminals.Count; i++) + { + compiled[i] = MediatorMiddlewareCompiler.Compile( + factoryCtx, terminals[i], middlewareConfigs, modifiers); + } + + notificationPipelines[notificationType] = compiled; } var pools = applicationServices.GetRequiredService(); - return new MediatorRuntime(pipelines.ToFrozenDictionary(), pools, features); + return new MediatorRuntime( + pipelines.ToFrozenDictionary(), + notificationPipelines.ToFrozenDictionary(), + pools, + features, + _options.NotificationPublishMode); + } + + private static MediatorDelegate BuildPipelineViaReflection(MediatorHandlerConfiguration config) + { + return config.Kind switch + { + MediatorHandlerKind.Command => + (MediatorDelegate)s_buildCommandPipeline + .MakeGenericMethod(config.HandlerType!, config.MessageType!) + .Invoke(null, null)!, + + MediatorHandlerKind.CommandResponse => + (MediatorDelegate)s_buildCommandResponsePipeline + .MakeGenericMethod(config.HandlerType!, config.MessageType!, config.ResponseType!) + .Invoke(null, null)!, + + MediatorHandlerKind.Query => + (MediatorDelegate)s_buildQueryPipeline + .MakeGenericMethod(config.HandlerType!, config.MessageType!, config.ResponseType!) + .Invoke(null, null)!, + + MediatorHandlerKind.Notification => + (MediatorDelegate)s_buildNotificationPipeline + .MakeGenericMethod(config.HandlerType!, config.MessageType!) + .Invoke(null, null)!, + + _ => throw new InvalidOperationException($"Unknown handler kind: {config.Kind}") + }; } /// diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs new file mode 100644 index 00000000000..5fea8ccd6b9 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs @@ -0,0 +1,61 @@ +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Mocha.Mediator; + +/// +/// Provides extension methods for registering mediator handlers through the host builder. +/// +public static class MediatorHostBuilderHandlerExtensions +{ + /// + /// Registers a handler with the mediator using descriptor-based configuration. + /// The handler type is inspected for , + /// , + /// , or + /// interfaces + /// and the appropriate pipeline is configured automatically. + /// + /// The handler implementation type. + /// The mediator host builder. + /// An optional action to configure the handler descriptor. + public static IMediatorHostBuilder AddHandler( + this IMediatorHostBuilder builder, + Action? configure = null) + where THandler : class + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.TryAdd(new ServiceDescriptor( + typeof(THandler), typeof(THandler), builder.Options.ServiceLifetime)); + + builder.ConfigureMediator(b => b.AddHandler(configure)); + + return builder; + } + + /// + /// Registers a handler with the mediator using a pre-built configuration. + /// This method is intended for use by source-generated code. + /// + /// The handler implementation type. + /// The mediator host builder. + /// The pre-built handler configuration. + [EditorBrowsable(EditorBrowsableState.Never)] + public static IMediatorHostBuilder AddHandlerConfiguration( + this IMediatorHostBuilder builder, + MediatorHandlerConfiguration configuration) + where THandler : class + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configuration); + + builder.Services.TryAdd(new ServiceDescriptor( + typeof(THandler), typeof(THandler), builder.Options.ServiceLifetime)); + + builder.ConfigureMediator(b => b.AddHandlerConfiguration(configuration)); + + return builder; + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/IHasMediatorConfigurationContext.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/IHasMediatorConfigurationContext.cs new file mode 100644 index 00000000000..b9d4ad2e7bf --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/IHasMediatorConfigurationContext.cs @@ -0,0 +1,12 @@ +namespace Mocha.Mediator; + +/// +/// Indicates that a descriptor or extension has access to the mediator configuration context. +/// +public interface IHasMediatorConfigurationContext +{ + /// + /// The descriptor context. + /// + IMediatorConfigurationContext Context { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorConfigurationContext.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorConfigurationContext.cs new file mode 100644 index 00000000000..4ede7614b2f --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorConfigurationContext.cs @@ -0,0 +1,17 @@ +namespace Mocha.Mediator; + +/// +/// Provides access to mediator configuration state during descriptor construction. +/// +public interface IMediatorConfigurationContext : IFeatureProvider +{ + /// + /// Gets the service provider used for resolving configuration-time dependencies. + /// + IServiceProvider Services { get; } + + /// + /// Gets the mediator options. + /// + MediatorOptions Options { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs new file mode 100644 index 00000000000..4ce2c4f27e1 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs @@ -0,0 +1,34 @@ +namespace Mocha.Mediator; + +/// +/// A typed mediator descriptor that provides access to extension points for the underlying configuration. +/// +/// The configuration type managed by this descriptor. +public interface IMediatorDescriptor : IMediatorDescriptor where T : MediatorConfiguration +{ + /// + /// Provides access to the underlying configuration. This is useful for extensions. + /// + new IMediatorDescriptorExtension Extend(); + + /// + /// Provides access to the underlying configuration. This is useful for extensions. + /// + IMediatorDescriptorExtension ExtendWith(Action> configure); + + /// + /// Provides access to the underlying configuration. This is useful for extensions. + /// + IMediatorDescriptorExtension ExtendWith(Action, TState> configure, TState state); +} + +/// +/// An untyped mediator descriptor that provides access to extension points for the underlying configuration. +/// +public interface IMediatorDescriptor +{ + /// + /// Provides access to the underlying configuration. This is useful for extensions. + /// + IMediatorDescriptorExtension Extend(); +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptorExtension.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptorExtension.cs new file mode 100644 index 00000000000..b75e5491bbd --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptorExtension.cs @@ -0,0 +1,26 @@ +namespace Mocha.Mediator; + +/// +/// Provides typed access to the underlying configuration of a descriptor for use by extensions. +/// +/// The configuration type. +public interface IMediatorDescriptorExtension : IMediatorDescriptorExtension where T : MediatorConfiguration +{ + /// + /// The type definition. + /// + new T Configuration { get; } + + MediatorConfiguration IMediatorDescriptorExtension.Configuration => Configuration; +} + +/// +/// Provides untyped access to the underlying configuration and context of a descriptor for use by extensions. +/// +public interface IMediatorDescriptorExtension : IHasMediatorConfigurationContext +{ + /// + /// The type definition. + /// + MediatorConfiguration Configuration { get; } +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorHandlerDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorHandlerDescriptor.cs new file mode 100644 index 00000000000..be9ddadf816 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorHandlerDescriptor.cs @@ -0,0 +1,6 @@ +namespace Mocha.Mediator; + +/// +/// Describes the configuration surface for a mediator handler. +/// +public interface IMediatorHandlerDescriptor : IMediatorDescriptor; diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorConfigurationContext.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorConfigurationContext.cs new file mode 100644 index 00000000000..52f99657628 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorConfigurationContext.cs @@ -0,0 +1,13 @@ +namespace Mocha.Mediator; + +internal sealed class MediatorConfigurationContext( + MediatorOptions options, + IServiceProvider services, + IFeatureCollection features) : IMediatorConfigurationContext +{ + public IServiceProvider Services => services; + + public MediatorOptions Options => options; + + public IFeatureCollection Features => features; +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs new file mode 100644 index 00000000000..b49fc8e963c --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs @@ -0,0 +1,39 @@ +namespace Mocha.Mediator; + +/// +/// Base class for mediator descriptors that provides extension point access and the configuration context. +/// +/// The configuration type this descriptor manages. +public abstract class MediatorDescriptorBase(IMediatorConfigurationContext context) + : IMediatorDescriptor + , IMediatorDescriptorExtension where T : MediatorConfiguration +{ + protected internal IMediatorConfigurationContext Context { get; } = + context ?? throw new ArgumentNullException(nameof(context)); + + IMediatorConfigurationContext IHasMediatorConfigurationContext.Context => Context; + + protected internal abstract T Configuration { get; protected set; } + + T IMediatorDescriptorExtension.Configuration => Configuration; + + public IMediatorDescriptorExtension Extend() => this; + + IMediatorDescriptorExtension IMediatorDescriptor.Extend() => Extend(); + + public IMediatorDescriptorExtension ExtendWith(Action> configure) + { + ArgumentNullException.ThrowIfNull(configure); + + configure(this); + return this; + } + + public IMediatorDescriptorExtension ExtendWith(Action, TState> configure, TState state) + { + ArgumentNullException.ThrowIfNull(configure); + + configure(this, state); + return this; + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs new file mode 100644 index 00000000000..1fbbdeab68b --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs @@ -0,0 +1,91 @@ +namespace Mocha.Mediator; + +/// +/// Provides a fluent API for configuring a mediator handler during setup. +/// Auto-detects handler kind by inspecting the handler type's interfaces. +/// +public class MediatorHandlerDescriptor + : MediatorDescriptorBase + , IMediatorHandlerDescriptor +{ + /// + /// Creates a new handler descriptor that auto-detects handler metadata + /// from the specified handler type's interfaces. + /// + /// The mediator configuration context. + /// The concrete handler implementation type. + public MediatorHandlerDescriptor(IMediatorConfigurationContext context, Type handlerType) + : base(context) + { + ArgumentNullException.ThrowIfNull(handlerType); + + Configuration = new MediatorHandlerConfiguration(); + Configuration.HandlerType = handlerType; + } + + protected internal override MediatorHandlerConfiguration Configuration { get; protected set; } + + /// + /// Builds and returns the finalized from this + /// descriptor's accumulated settings. If no configuration was applied via + /// , auto-detects handler metadata + /// from the handler type's interfaces. + /// + public MediatorHandlerConfiguration CreateConfiguration() + { + if (Configuration.MessageType is null) + { + DetectHandler(Configuration.HandlerType!); + } + + return Configuration; + } + + private void DetectHandler(Type handlerType) + { + foreach (var @interface in handlerType.GetInterfaces()) + { + if (!@interface.IsGenericType) + { + continue; + } + + var genericDef = @interface.GetGenericTypeDefinition(); + + if (genericDef == typeof(ICommandHandler<,>)) + { + var args = @interface.GetGenericArguments(); + Configuration.MessageType = args[0]; + Configuration.ResponseType = args[1]; + Configuration.Kind = MediatorHandlerKind.CommandResponse; + return; + } + + if (genericDef == typeof(ICommandHandler<>)) + { + Configuration.MessageType = @interface.GetGenericArguments()[0]; + Configuration.Kind = MediatorHandlerKind.Command; + return; + } + + if (genericDef == typeof(IQueryHandler<,>)) + { + var args = @interface.GetGenericArguments(); + Configuration.MessageType = args[0]; + Configuration.ResponseType = args[1]; + Configuration.Kind = MediatorHandlerKind.Query; + return; + } + + if (genericDef == typeof(INotificationHandler<>)) + { + Configuration.MessageType = @interface.GetGenericArguments()[0]; + Configuration.Kind = MediatorHandlerKind.Notification; + return; + } + } + + throw new InvalidOperationException( + $"Type '{handlerType}' does not implement any known mediator handler interface."); + } +} diff --git a/src/Mocha/src/Mocha.Mediator/Mediator.cs b/src/Mocha/src/Mocha.Mediator/Mediator.cs index 4296ac61539..050ce3f63f0 100644 --- a/src/Mocha/src/Mocha.Mediator/Mediator.cs +++ b/src/Mocha/src/Mocha.Mediator/Mediator.cs @@ -118,33 +118,41 @@ public ValueTask PublishAsync( { ArgumentNullException.ThrowIfNull(notification); - var messageType = notification.GetType(); - var pipeline = runtime.GetPipeline(messageType); - var context = runtime.RentContext(); + return PublishCoreAsync(notification, notification.GetType(), cancellationToken); + } - context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); + /// + ValueTask IPublisher.PublishAsync(object notification, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(notification); - var task = pipeline(context); + return PublishCoreAsync(notification, notification.GetType(), cancellationToken); + } - if (task.IsCompletedSuccessfully) - { - runtime.ReturnContext(context); + private ValueTask PublishCoreAsync( + object notification, + Type messageType, + CancellationToken cancellationToken) + { + var pipelines = runtime.GetNotificationPipelines(messageType); - return default; + if (pipelines.Length == 1) + { + return PublishSingle(pipelines[0], notification, messageType, cancellationToken); } - return AwaitAndReturn(task, context); + return runtime.NotificationPublishMode == NotificationPublishMode.Concurrent + ? PublishConcurrently(pipelines, notification, messageType, cancellationToken) + : PublishSequentially(pipelines, notification, messageType, cancellationToken); } - /// - ValueTask IPublisher.PublishAsync(object notification, CancellationToken cancellationToken) + private ValueTask PublishSingle( + MediatorDelegate pipeline, + object notification, + Type messageType, + CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(notification); - - var messageType = notification.GetType(); - var pipeline = runtime.GetPipeline(messageType); var context = runtime.RentContext(); - context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); var task = pipeline(context); @@ -158,6 +166,76 @@ ValueTask IPublisher.PublishAsync(object notification, CancellationToken cancell return AwaitAndReturn(task, context); } + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask PublishSequentially( + MediatorDelegate[] pipelines, + object notification, + Type messageType, + CancellationToken cancellationToken) + { + for (var i = 0; i < pipelines.Length; i++) + { + var context = runtime.RentContext(); + try + { + context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); + await pipelines[i](context).ConfigureAwait(false); + } + finally + { + runtime.ReturnContext(context); + } + } + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask PublishConcurrently( + MediatorDelegate[] pipelines, + object notification, + Type messageType, + CancellationToken cancellationToken) + { + var count = pipelines.Length; + var contexts = new MediatorContext[count]; + var tasks = new Task[count]; + + for (var i = 0; i < count; i++) + { + var context = runtime.RentContext(); + context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); + contexts[i] = context; + tasks[i] = pipelines[i](context).AsTask(); + } + + try + { + var whenAll = Task.WhenAll(tasks); + + try + { + await whenAll.ConfigureAwait(false); + } + catch + { + // Task.WhenAll captures all exceptions, but await unwraps only the first. + // Re-throw the AggregateException to surface all failures. + if (whenAll.Exception is not null) + { + throw whenAll.Exception; + } + + throw; + } + } + finally + { + for (var i = 0; i < count; i++) + { + runtime.ReturnContext(contexts[i]); + } + } + } + [MethodImpl(MethodImplOptions.NoInlining)] [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] private async ValueTask AwaitAndReturn(ValueTask task, MediatorContext context) diff --git a/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs b/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs index 77017811dbe..ebb9ca5a70b 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs @@ -12,4 +12,33 @@ public sealed class MediatorOptions /// Default is . /// public ServiceLifetime ServiceLifetime { get; set; } = ServiceLifetime.Scoped; + + /// + /// Gets or sets how notification handler pipelines are dispatched. + /// Default is . + /// + public NotificationPublishMode NotificationPublishMode { get; set; } = NotificationPublishMode.Sequential; +} + +/// +/// Specifies how notification handler pipelines are dispatched. +/// +public enum NotificationPublishMode +{ + /// + /// Handlers are invoked sequentially, awaiting each before proceeding to the next. + /// If a handler throws, subsequent handlers are not invoked. + /// + Sequential, + + /// + /// Handlers are invoked concurrently using . + /// All handlers are started simultaneously and awaited together. + /// + /// Warning: All concurrent handler pipelines share the same scoped + /// . Scoped services such as DbContext + /// are not thread-safe and must not be used concurrently across handlers. + /// + /// + Concurrent } diff --git a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs index d63da72c930..84038dcdfc8 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs @@ -11,6 +11,7 @@ namespace Mocha.Mediator; public sealed class MediatorRuntime : IMediatorRuntime { private readonly FrozenDictionary _pipelines; + private readonly FrozenDictionary _notificationPipelines; private readonly ObjectPool _contextPool; [ThreadStatic] @@ -18,12 +19,16 @@ public sealed class MediatorRuntime : IMediatorRuntime internal MediatorRuntime( FrozenDictionary pipelines, + FrozenDictionary notificationPipelines, IMediatorPools pools, - IFeatureCollection features) + IFeatureCollection features, + NotificationPublishMode notificationPublishMode) { _pipelines = pipelines; + _notificationPipelines = notificationPipelines; _contextPool = pools.MediatorContext; Features = features; + NotificationPublishMode = notificationPublishMode; } /// @@ -31,6 +36,11 @@ internal MediatorRuntime( /// public IFeatureCollection Features { get; } + /// + /// Gets the notification publish mode for this mediator runtime. + /// + internal NotificationPublishMode NotificationPublishMode { get; } + /// /// Gets the compiled pipeline delegate for the specified message type. /// @@ -80,8 +90,28 @@ public void ReturnContext(MediatorContext context) } } + /// + /// Gets the compiled notification pipeline delegates for the specified notification type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MediatorDelegate[] GetNotificationPipelines(Type notificationType) + { + if (_notificationPipelines.TryGetValue(notificationType, out var pipelines)) + { + return pipelines; + } + + return ThrowMissingNotificationPipeline(notificationType); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static MediatorDelegate ThrowMissingPipeline(Type messageType) => throw new InvalidOperationException( $"No pipeline registered for message type {messageType}"); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static MediatorDelegate[] ThrowMissingNotificationPipeline(Type notificationType) + => throw new InvalidOperationException( + $"No notification pipeline registered for message type {notificationType}. " + + "If this is a command or query, use SendAsync or QueryAsync instead."); } diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs deleted file mode 100644 index 5e90357a51a..00000000000 --- a/src/Mocha/src/Mocha.Mediator/Pipeline/ForeachAwaitPublisher.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Mocha.Mediator; - -/// -/// Represents a notification publishing strategy that dispatches to each handler sequentially, -/// awaiting completion before proceeding to the next. -/// -public sealed class ForeachAwaitPublisher : INotificationStrategy -{ - /// - /// Publishes a notification to each handler in sequence, awaiting each one before proceeding. - /// - /// The type of notification to publish. - /// The collection of handlers to notify. - /// The notification instance to publish. - /// A token to observe for cancellation requests. - /// A representing the asynchronous operation. - public ValueTask PublishAsync( - IReadOnlyList> handlers, - TNotification notification, - CancellationToken cancellationToken) - where TNotification : INotification - { - var count = handlers.Count; - - if (count == 0) - { - return default; - } - - if (count == 1) - { - return handlers[0].HandleAsync(notification, cancellationToken); - } - - return PublishSequentially(handlers, notification, cancellationToken, count); - } - - private static async ValueTask PublishSequentially( - IReadOnlyList> handlers, - TNotification notification, - CancellationToken cancellationToken, - int count) - where TNotification : INotification - { - for (var i = 0; i < count; i++) - { - await handlers[i].HandleAsync(notification, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs deleted file mode 100644 index 351504660f7..00000000000 --- a/src/Mocha/src/Mocha.Mediator/Pipeline/MediatorPipelineConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.ComponentModel; - -namespace Mocha.Mediator; - -/// -/// Describes a pipeline to be compiled for a specific message type. -/// Carries all metadata needed by the middleware compiler so it does not -/// have to derive information from the message type at runtime. -/// This type is intended for use by source-generated code. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class MediatorPipelineConfiguration -{ - /// - /// Gets the message type this pipeline handles. - /// - public required Type MessageType { get; init; } - - /// - /// Gets the response type produced by the handler, - /// or for void commands and notifications. - /// For stream handlers this is IAsyncEnumerable<TResponse>. - /// - public Type? ResponseType { get; init; } - - /// - /// Gets the terminal delegate that invokes the handler. - /// This is the innermost layer of the middleware pipeline. - /// - public required MediatorDelegate Terminal { get; init; } -} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs deleted file mode 100644 index 3eebc024c58..00000000000 --- a/src/Mocha/src/Mocha.Mediator/Pipeline/NotificationStrategyFeature.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Mocha.Mediator; - -/// -/// Feature that carries the resolved on the mediator runtime. -/// -internal sealed class NotificationStrategyFeature(INotificationStrategy strategy) -{ - public INotificationStrategy Strategy { get; } = strategy; -} diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs index 243e2568ee2..e49a8a859e2 100644 --- a/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs +++ b/src/Mocha/src/Mocha.Mediator/Pipeline/PipelineBuilder.cs @@ -6,7 +6,7 @@ namespace Mocha.Mediator; /// -/// Provides terminal delegate factories for each handler kind. +/// Provides pipeline delegate factories for each handler kind. /// These delegates form the innermost layer of the middleware pipeline, /// resolving handlers from the scoped service provider and invoking them. /// This class is intended for use by source-generated code. @@ -15,16 +15,15 @@ namespace Mocha.Mediator; public static class PipelineBuilder { /// - /// Builds a terminal delegate for a void command handler. + /// Builds a pipeline delegate for a void command handler. /// - public static MediatorDelegate BuildVoidCommandTerminal() + public static MediatorDelegate BuildCommandPipeline() + where THandler : class, ICommandHandler where TCommand : ICommand { - var serviceType = typeof(ICommandHandler); - - return ctx => + return static ctx => { - var handler = (ICommandHandler)ctx.Services.GetRequiredService(serviceType); + var handler = ctx.Services.GetRequiredService(); var task = handler.HandleAsync((TCommand)ctx.Message, ctx.CancellationToken); if (task.IsCompletedSuccessfully) @@ -44,16 +43,15 @@ static async ValueTask Awaited(ValueTask t) } /// - /// Builds a terminal delegate for a command handler that returns a response. + /// Builds a pipeline delegate for a command handler that returns a response. /// - public static MediatorDelegate BuildCommandTerminal() + public static MediatorDelegate BuildCommandResponsePipeline() + where THandler : class, ICommandHandler where TCommand : ICommand { - var serviceType = typeof(ICommandHandler); - - return ctx => + return static ctx => { - var handler = (ICommandHandler)ctx.Services.GetRequiredService(serviceType); + var handler = ctx.Services.GetRequiredService(); var task = handler.HandleAsync((TCommand)ctx.Message, ctx.CancellationToken); if (task.IsCompletedSuccessfully) @@ -74,16 +72,15 @@ static async ValueTask Awaited(ValueTask t, IMediatorContext c) } /// - /// Builds a terminal delegate for a query handler. + /// Builds a pipeline delegate for a query handler. /// - public static MediatorDelegate BuildQueryTerminal() + public static MediatorDelegate BuildQueryPipeline() + where THandler : class, IQueryHandler where TQuery : IQuery { - var serviceType = typeof(IQueryHandler); - - return ctx => + return static ctx => { - var handler = (IQueryHandler)ctx.Services.GetRequiredService(serviceType); + var handler = ctx.Services.GetRequiredService(); var task = handler.HandleAsync((TQuery)ctx.Message, ctx.CancellationToken); if (task.IsCompletedSuccessfully) @@ -104,47 +101,16 @@ static async ValueTask Awaited(ValueTask t, IMediatorContext c) } /// - /// Builds a terminal delegate for a notification with handler types known at compile time. + /// Builds a pipeline delegate for a single notification handler. /// - public static MediatorDelegate BuildNotificationTerminal(Type[] handlerTypes) + public static MediatorDelegate BuildNotificationPipeline() + where THandler : class, INotificationHandler where TNotification : INotification { - if (handlerTypes.Length == 1) - { - var handlerType = handlerTypes[0]; - return ctx => - { - var handler = (INotificationHandler)ctx.Services.GetRequiredService(handlerType); - var task = handler.HandleAsync((TNotification)ctx.Message, ctx.CancellationToken); - - if (task.IsCompletedSuccessfully) - { - return default; - } - - return Awaited(task); - - [MethodImpl(MethodImplOptions.NoInlining)] - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - static async ValueTask Awaited(ValueTask t) - { - await t.ConfigureAwait(false); - } - }; - } - - return ctx => + return static ctx => { - var strategy = ctx.Runtime.Features - .GetRequired().Strategy; - - var handlers = new INotificationHandler[handlerTypes.Length]; - for (var i = 0; i < handlerTypes.Length; i++) - { - handlers[i] = (INotificationHandler)ctx.Services.GetRequiredService(handlerTypes[i]); - } - - var task = strategy.PublishAsync(handlers, (TNotification)ctx.Message, ctx.CancellationToken); + var handler = ctx.Services.GetRequiredService(); + var task = handler.HandleAsync((TNotification)ctx.Message, ctx.CancellationToken); if (task.IsCompletedSuccessfully) { diff --git a/src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs b/src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs deleted file mode 100644 index df3dd75001c..00000000000 --- a/src/Mocha/src/Mocha.Mediator/Pipeline/TaskWhenAllPublisher.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Mocha.Mediator; - -/// -/// Represents a notification publishing strategy that dispatches to all handlers concurrently -/// using . -/// -public sealed class TaskWhenAllPublisher : INotificationStrategy -{ - /// - /// Publishes a notification to all handlers concurrently, awaiting all completions. - /// - /// The type of notification to publish. - /// The collection of handlers to notify. - /// The notification instance to publish. - /// A token to observe for cancellation requests. - /// A representing the asynchronous operation. - public ValueTask PublishAsync( - IReadOnlyList> handlers, - TNotification notification, - CancellationToken cancellationToken) - where TNotification : INotification - { - var count = handlers.Count; - - if (count == 0) - { - return default; - } - - if (count == 1) - { - return handlers[0].HandleAsync(notification, cancellationToken); - } - - return PublishConcurrently(handlers, notification, cancellationToken, count); - } - - private static async ValueTask PublishConcurrently( - IReadOnlyList> handlers, - TNotification notification, - CancellationToken cancellationToken, - int count) - where TNotification : INotification - { - var tasks = new Task[count]; - - for (var i = 0; i < count; i++) - { - tasks[i] = handlers[i].HandleAsync(notification, cancellationToken).AsTask(); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - } -} diff --git a/src/Mocha/src/Mocha/Descriptors/IMessagingDescriptor.cs b/src/Mocha/src/Mocha/Descriptors/IMessagingDescriptor.cs index 43c975dcec5..0bc93227492 100644 --- a/src/Mocha/src/Mocha/Descriptors/IMessagingDescriptor.cs +++ b/src/Mocha/src/Mocha/Descriptors/IMessagingDescriptor.cs @@ -9,17 +9,17 @@ public interface IMessagingDescriptor : IMessagingDescriptor where T : Me /// /// Provides access to the underlying configuration. This is useful for extensions. /// - new IDescriptorExtension Extend(); + new IMessagingDescriptorExtension Extend(); /// /// Provides access to the underlying configuration. This is useful for extensions. /// - IDescriptorExtension ExtendWith(Action> configure); + IMessagingDescriptorExtension ExtendWith(Action> configure); /// /// Provides access to the underlying configuration. This is useful for extensions. /// - IDescriptorExtension ExtendWith(Action, TState> configure, TState state); + IMessagingDescriptorExtension ExtendWith(Action, TState> configure, TState state); } /// @@ -30,5 +30,5 @@ public interface IMessagingDescriptor /// /// Provides access to the underlying configuration. This is useful for extensions. /// - IDescriptorExtension Extend(); + IMessagingDescriptorExtension Extend(); } diff --git a/src/Mocha/src/Mocha/Descriptors/IDescriptorExtension.cs b/src/Mocha/src/Mocha/Descriptors/IMessagingDescriptorExtension.cs similarity index 66% rename from src/Mocha/src/Mocha/Descriptors/IDescriptorExtension.cs rename to src/Mocha/src/Mocha/Descriptors/IMessagingDescriptorExtension.cs index 5dede61a3cb..60645c63714 100644 --- a/src/Mocha/src/Mocha/Descriptors/IDescriptorExtension.cs +++ b/src/Mocha/src/Mocha/Descriptors/IMessagingDescriptorExtension.cs @@ -4,20 +4,20 @@ namespace Mocha; /// Provides typed access to the underlying configuration of a descriptor for use by extensions. /// /// The configuration type. -public interface IDescriptorExtension : IDescriptorExtension where T : MessagingConfiguration +public interface IMessagingDescriptorExtension : IMessagingDescriptorExtension where T : MessagingConfiguration { /// /// The type definition. /// new T Configuration { get; } - MessagingConfiguration IDescriptorExtension.Configuration => Configuration; + MessagingConfiguration IMessagingDescriptorExtension.Configuration => Configuration; } /// /// Provides untyped access to the underlying configuration and context of a descriptor for use by extensions. /// -public interface IDescriptorExtension : IHasConfigurationContext +public interface IMessagingDescriptorExtension : IHasConfigurationContext { /// /// The type definition. diff --git a/src/Mocha/src/Mocha/Descriptors/MessagingDescriptorBase.cs b/src/Mocha/src/Mocha/Descriptors/MessagingDescriptorBase.cs index c52f41dac44..2512f4dd6e9 100644 --- a/src/Mocha/src/Mocha/Descriptors/MessagingDescriptorBase.cs +++ b/src/Mocha/src/Mocha/Descriptors/MessagingDescriptorBase.cs @@ -6,7 +6,7 @@ namespace Mocha; /// The configuration type this descriptor manages. public abstract class MessagingDescriptorBase(IMessagingConfigurationContext context) : IMessagingDescriptor - , IDescriptorExtension where T : MessagingConfiguration + , IMessagingDescriptorExtension where T : MessagingConfiguration { protected internal IMessagingConfigurationContext Context { get; } = context ?? throw new ArgumentNullException(nameof(context)); @@ -15,13 +15,13 @@ public abstract class MessagingDescriptorBase(IMessagingConfigurationContext protected internal abstract T Configuration { get; protected set; } - T IDescriptorExtension.Configuration => Configuration; + T IMessagingDescriptorExtension.Configuration => Configuration; - public IDescriptorExtension Extend() => this; + public IMessagingDescriptorExtension Extend() => this; - IDescriptorExtension IMessagingDescriptor.Extend() => Extend(); + IMessagingDescriptorExtension IMessagingDescriptor.Extend() => Extend(); - public IDescriptorExtension ExtendWith(Action> configure) + public IMessagingDescriptorExtension ExtendWith(Action> configure) { ArgumentNullException.ThrowIfNull(configure); @@ -29,7 +29,7 @@ public IDescriptorExtension ExtendWith(Action> config return this; } - public IDescriptorExtension ExtendWith(Action, TState> configure, TState state) + public IMessagingDescriptorExtension ExtendWith(Action, TState> configure, TState state) { ArgumentNullException.ThrowIfNull(configure); diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs index 24e11ba2d27..8f9a00e76d0 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs @@ -215,8 +215,8 @@ public IMessagingTransportDescriptor UseReceive( /// Returns this descriptor as an extension point for the transport configuration, allowing additional /// configuration to be layered by external modules. /// - /// This descriptor cast as . - public new IDescriptorExtension Extend() + /// This descriptor cast as . + public new IMessagingDescriptorExtension Extend() { return this; } @@ -225,9 +225,9 @@ public IMessagingTransportDescriptor UseReceive( /// Applies an extension configuration delegate to this transport descriptor. /// /// A delegate that configures the transport through the extension interface. - /// This descriptor cast as . - public IDescriptorExtension ExtendWith( - Action> configure) + /// This descriptor cast as . + public IMessagingDescriptorExtension ExtendWith( + Action> configure) { return this; } @@ -238,9 +238,9 @@ public IDescriptorExtension ExtendWith( /// The type of the state object passed to the delegate. /// A delegate that configures the transport through the extension interface using the provided state. /// The state object forwarded to the delegate. - /// This descriptor cast as . - public IDescriptorExtension ExtendWith( - Action, TState> configure, + /// This descriptor cast as . + public IMessagingDescriptorExtension ExtendWith( + Action, TState> configure, TState state) { return this; diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md index fe5c2eaffdb..654d92630ef 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.CreateOrderHandler), MessageType = typeof(global::TestApp.CreateOrderCommand), ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md index 410dbeb877e..00348330827 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md @@ -14,28 +14,25 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.CreateOrderCommand), - ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + HandlerType = typeof(global::TestApp.DeleteOrderHandler), + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + HandlerType = typeof(global::TestApp.CreateOrderHandler), + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md index 70db30bd4dc..6df93870771 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.DeleteOrderHandler), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md index 9141fd0c4e9..ba5dc5bd496 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md @@ -16,29 +16,26 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandlerA), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandlerB), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.CreateOrderHandlerA), MessageType = typeof(global::TestApp.CreateOrderCommand), ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.CreateOrderHandlerB), MessageType = typeof(global::TestApp.CreateOrderCommand), ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md index f6e58be366c..d4b08ea56db 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md @@ -16,27 +16,24 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandlerA), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandlerB), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.DeleteOrderHandlerA), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.DeleteOrderHandlerB), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md index c46621cff0d..7648ba7cb15 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md @@ -16,21 +16,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler>), typeof(global::TestApp.GenericCommandHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration>(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GenericCommandHandler), MessageType = typeof(global::TestApp.GenericCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal>() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline, global::TestApp.GenericCommand>() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md index 0e179f97bda..79a9e35d339 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md @@ -16,22 +16,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler, T>), typeof(global::TestApp.GenericQueryHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration>(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GenericQueryHandler), MessageType = typeof(global::TestApp.GenericQuery), ResponseType = typeof(T), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal, T>() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline, global::TestApp.GenericQuery, T>() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md index 68a1e39dfd0..58fa8e23493 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.DeleteOrderHandler), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md index 430222bdaf2..7ed08a51542 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTest2( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.DeleteOrderHandler), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md index 3621633e3e4..5ba352e34ae 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md @@ -14,33 +14,32 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.AlphaHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.MidHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ZetaHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.AlphaHandler), MessageType = typeof(global::TestApp.AlphaCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.MidHandler), MessageType = typeof(global::TestApp.MidCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.ZetaHandler), MessageType = typeof(global::TestApp.ZetaCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md index 3102c237869..0fe2f8de85e 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md @@ -16,22 +16,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler, string>), typeof(global::TestApp.StringProcessor), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.StringProcessor), MessageType = typeof(global::TestApp.ProcessCommand), ResponseType = typeof(string), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal, string>() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline, string>() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md index 7ba89aea38e..815a1841400 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTest( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.DeleteOrderHandler), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md index 6821e52fcd6..c3658a948c2 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ComputeHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.ComputeHandler), MessageType = typeof(global::TestApp.ComputeCommand), ResponseType = typeof(long), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md index 3b4736118fb..ceb0ea09039 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.FireAndForgetHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.FireAndForgetHandler), MessageType = typeof(global::TestApp.FireAndForgetCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md index f9e0b887e29..d73bd07dc3c 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md @@ -14,43 +14,42 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ResponseCommandHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.VoidCommandHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.MyQueryHandler), lifetime)); - - // Register notification handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.MyEventHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.ResponseCommand), - ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + HandlerType = typeof(global::TestApp.VoidCommandHandler), + MessageType = typeof(global::TestApp.VoidCommand), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.VoidCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + HandlerType = typeof(global::TestApp.ResponseCommandHandler), + MessageType = typeof(global::TestApp.ResponseCommand), + ResponseType = typeof(int), + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.MyQueryHandler), MessageType = typeof(global::TestApp.MyQuery), ResponseType = typeof(string), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.MyEventHandler), MessageType = typeof(global::TestApp.MyEvent), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.MyEventHandler) }) + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md index fe8de69d484..72c045f859b 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetItemHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetItemHandler), MessageType = typeof(global::TestApp.GetItemQuery), ResponseType = typeof(string), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md index 43b370701c5..e6fcf81e6b8 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddOrdering( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.PingHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.PingHandler), MessageType = typeof(global::TestApp.PingCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md index d59668ec973..3d9cdda9749 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateInvoiceHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.CreateInvoiceHandler), MessageType = typeof(global::TestApp.CreateInvoiceCommand), ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md index f21d20f3a66..bf2d65a1323 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md @@ -14,44 +14,50 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteOrderHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetUserHandler), lifetime)); - - // Register notification handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.OrderCreatedEmailHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.OrderCreatedStatsHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.CreateOrderCommand), - ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + HandlerType = typeof(global::TestApp.DeleteOrderHandler), + MessageType = typeof(global::TestApp.DeleteOrderCommand), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + HandlerType = typeof(global::TestApp.CreateOrderHandler), + MessageType = typeof(global::TestApp.CreateOrderCommand), + ResponseType = typeof(int), + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetUserHandler), MessageType = typeof(global::TestApp.GetUserQuery), ResponseType = typeof(global::TestApp.UserDto), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() + }); + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration + { + HandlerType = typeof(global::TestApp.OrderCreatedEmailHandler), + MessageType = typeof(global::TestApp.OrderCreated), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.OrderCreatedStatsHandler), MessageType = typeof(global::TestApp.OrderCreated), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.OrderCreatedEmailHandler), typeof(global::TestApp.OrderCreatedStatsHandler) }) + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md index e650b0f3d7f..64c2a35c3fc 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md @@ -14,29 +14,26 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.Orders.CreateOrderHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.Users.GetUserHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.Orders.CreateOrderHandler), MessageType = typeof(global::TestApp.Orders.CreateOrderCommand), ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.Users.GetUserHandler), MessageType = typeof(global::TestApp.Users.GetUserQuery), ResponseType = typeof(global::TestApp.Users.UserDto), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md index 1c863cb9cbb..0cbb349af3e 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddOrder_Processing( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.PingHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.PingHandler), MessageType = typeof(global::TestApp.PingCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md index 9a83b4cf1f1..e1d6e37457a 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddUnknown( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.PingHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.PingHandler), MessageType = typeof(global::TestApp.PingCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md index 053cf099324..212d88fc672 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.Outer.DeleteOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.Outer.DeleteOrderHandler), MessageType = typeof(global::TestApp.DeleteOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md index 6be1a697c41..1412f566583 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md @@ -14,22 +14,24 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register notification handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.SendEmailHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.UpdateStatsHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration + { + HandlerType = typeof(global::TestApp.SendEmailHandler), + MessageType = typeof(global::TestApp.OrderCreated), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() + }); + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.UpdateStatsHandler), MessageType = typeof(global::TestApp.OrderCreated), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.SendEmailHandler), typeof(global::TestApp.UpdateStatsHandler) }) + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md index c835f34e560..54e548c8e27 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register notification handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.OrderCreatedEmailHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.OrderCreatedEmailHandler), MessageType = typeof(global::TestApp.OrderCreated), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.OrderCreatedEmailHandler) }) + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md index bbd99eb3a4b..c64aed506ac 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.CreateOrderHandler), MessageType = typeof(global::TestApp.CreateOrderCommand), ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md index e63a3ad4784..294e0e41b81 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetOrderQueryHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetOrderQueryHandler), MessageType = typeof(global::TestApp.GetOrderQuery), ResponseType = typeof(string), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md index ae310054729..f68357c1540 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md @@ -14,21 +14,16 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.ProcessOrderHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.ProcessOrderHandler), MessageType = typeof(global::TestApp.ProcessOrderCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md index 2b6727c1596..bc05067d0f0 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md @@ -14,29 +14,26 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetOrderHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetUserHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetOrderHandler), MessageType = typeof(global::TestApp.GetOrderQuery), ResponseType = typeof(global::TestApp.OrderDto), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetUserHandler), MessageType = typeof(global::TestApp.GetUserQuery), ResponseType = typeof(global::TestApp.UserDto), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md index 4da6331ab91..d9e1649ad9b 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md @@ -14,22 +14,17 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetUserHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetUserHandler), MessageType = typeof(global::TestApp.GetUserQuery), ResponseType = typeof(global::TestApp.UserDto), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md index f9bf510dad3..d94e843cedf 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md @@ -14,43 +14,42 @@ namespace Microsoft.Extensions.DependencyInjection public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( this global::Mocha.Mediator.IMediatorHostBuilder builder) { - var services = builder.Services; - var lifetime = builder.Options.ServiceLifetime; - // Register handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.CreateItemHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.ICommandHandler), typeof(global::TestApp.DeleteItemHandler), lifetime)); - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.IQueryHandler), typeof(global::TestApp.GetItemHandler), lifetime)); - - // Register notification handlers - global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddEnumerable(services, new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(global::Mocha.Mediator.INotificationHandler), typeof(global::TestApp.ItemCreatedHandler), lifetime)); - - // Register pipelines - global::Mocha.Mediator.MediatorHostBuilderExtensions.ConfigureMediator(builder, static b => - { - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.CreateItemCommand), - ResponseType = typeof(int), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildCommandTerminal() + HandlerType = typeof(global::TestApp.DeleteItemHandler), + MessageType = typeof(global::TestApp.DeleteItemCommand), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Command, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { - MessageType = typeof(global::TestApp.DeleteItemCommand), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildVoidCommandTerminal() + HandlerType = typeof(global::TestApp.CreateItemHandler), + MessageType = typeof(global::TestApp.CreateItemCommand), + ResponseType = typeof(int), + Kind = global::Mocha.Mediator.MediatorHandlerKind.CommandResponse, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandResponsePipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.GetItemHandler), MessageType = typeof(global::TestApp.GetItemQuery), ResponseType = typeof(global::TestApp.ItemDto), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildQueryTerminal() + Kind = global::Mocha.Mediator.MediatorHandlerKind.Query, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline() }); - b.RegisterPipeline(new global::Mocha.Mediator.MediatorPipelineConfiguration + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration { + HandlerType = typeof(global::TestApp.ItemCreatedHandler), MessageType = typeof(global::TestApp.ItemCreated), - Terminal = global::Mocha.Mediator.PipelineBuilder.BuildNotificationTerminal(new global::System.Type[] { typeof(global::TestApp.ItemCreatedHandler) }) + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() }); - }); return builder; } diff --git a/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs b/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs index 0ea3ffd8fa2..a1438915912 100644 --- a/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs +++ b/src/Mocha/test/Mocha.EntityFrameworkCore.Tests/EntityFrameworkTransactionMiddlewareTests.cs @@ -23,23 +23,13 @@ public EntityFrameworkTransactionMiddlewareTests() var builder = services.AddMediator() .UseEntityFrameworkTransactions(); - services.AddTransient, CreateItemHandler>(); - services.AddTransient, CreateItemWithResponseHandler>(); + services.AddTransient(); + services.AddTransient(); - // Register pipelines (normally done by source-generated code) builder.ConfigureMediator(b => { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CreateItemCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CreateItemWithResponseCommand), - ResponseType = typeof(int), - Terminal = PipelineBuilder.BuildCommandTerminal() - }); + b.AddHandler(); + b.AddHandler(); }); _provider = services.BuildServiceProvider(); @@ -92,13 +82,9 @@ public async Task Rolls_Back_Transaction_On_Failure() var services = new ServiceCollection(); services.AddDbContext(o => o.UseSqlite(_connection)); var builder = services.AddMediator().UseEntityFrameworkTransactions(); - services.AddTransient, FailingCreateItemHandler>(); + services.AddTransient(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CreateItemCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); var runtime = provider.GetRequiredService(); @@ -188,14 +174,9 @@ public async Task Query_Skips_Transaction_By_Default() var services = new ServiceCollection(); services.AddDbContext(o => o.UseSqlite(_connection)); var builder = services.AddMediator().UseEntityFrameworkTransactions(); - services.AddTransient>, GetItemsHandler>(); + services.AddTransient(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(GetItemsQuery), - ResponseType = typeof(List), - Terminal = PipelineBuilder.BuildQueryTerminal>() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); @@ -241,14 +222,9 @@ public async Task ShouldCreateTransaction_Override_Enables_Query_Transactions() options.ShouldCreateTransaction = _ => true; }); - services.AddTransient>, GetItemsHandler>(); + services.AddTransient(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(GetItemsQuery), - ResponseType = typeof(List), - Terminal = PipelineBuilder.BuildQueryTerminal>() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); diff --git a/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs new file mode 100644 index 00000000000..e86994af848 --- /dev/null +++ b/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs @@ -0,0 +1,198 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mocha.Mediator.Tests; + +public class AddHandlerTests +{ + private static IServiceProvider BuildProvider(Action configure) + { + var services = new ServiceCollection(); + var builder = services.AddMediator(); + configure(builder); + return services.BuildServiceProvider(); + } + + [Fact] + public async Task AddHandler_Should_DispatchVoidCommand() + { + // Arrange + ManualVoidCommandHandler.WasInvoked = false; + var sp = BuildProvider(b => b.AddHandler()); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.SendAsync(new ManualVoidCommand("test")); + + // Assert + Assert.True(ManualVoidCommandHandler.WasInvoked); + } + + [Fact] + public async Task AddHandler_Should_DispatchCommandWithResponse() + { + // Arrange + var sp = BuildProvider(b => b.AddHandler()); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.SendAsync(new ManualCommand("hello")); + + // Assert + Assert.NotNull(result); + Assert.Equal("manual-hello", result.Data); + } + + [Fact] + public async Task AddHandler_Should_DispatchQuery() + { + // Arrange + var sp = BuildProvider(b => b.AddHandler()); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + var result = await mediator.QueryAsync(new ManualQuery(42)); + + // Assert + Assert.NotNull(result); + Assert.Equal("query-42", result.Data); + } + + [Fact] + public async Task AddHandler_Should_DispatchNotification_When_SingleHandler() + { + // Arrange + ManualNotificationHandler1.WasInvoked = false; + var sp = BuildProvider(b => b.AddHandler()); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.PublishAsync(new ManualNotification("ping")); + + // Assert + Assert.True(ManualNotificationHandler1.WasInvoked); + } + + [Fact] + public async Task AddHandler_Should_DispatchToAllHandlers_When_MultipleNotificationHandlers() + { + // Arrange + ManualNotificationHandler1.WasInvoked = false; + ManualNotificationHandler2.WasInvoked = false; + var sp = BuildProvider(b => + { + b.AddHandler(); + b.AddHandler(); + }); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act + await mediator.PublishAsync(new ManualNotification("fan-out")); + + // Assert + Assert.True(ManualNotificationHandler1.WasInvoked); + Assert.True(ManualNotificationHandler2.WasInvoked); + } + + [Fact] + public void AddHandler_Should_Throw_When_TypeIsNotHandler() + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddMediator(); + builder.AddHandler(); + using var sp = services.BuildServiceProvider(); + + // Act & Assert -- validation is deferred to Build() time, which happens + // when MediatorRuntime is first resolved (triggered by IMediator resolution). + Assert.Throws( + () => sp.GetRequiredService()); + } + + [Fact] + public async Task AddHandler_Should_SupportFluentChaining() + { + // Arrange + ManualVoidCommandHandler.WasInvoked = false; + var sp = BuildProvider(b => + b.AddHandler() + .AddHandler() + .AddHandler() + .AddHandler()); + using var scope = sp.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + // Act & Assert — all handler types work together + await mediator.SendAsync(new ManualVoidCommand("v")); + Assert.True(ManualVoidCommandHandler.WasInvoked); + + var cmdResult = await mediator.SendAsync(new ManualCommand("c")); + Assert.Equal("manual-c", cmdResult.Data); + + var queryResult = await mediator.QueryAsync(new ManualQuery(7)); + Assert.Equal("query-7", queryResult.Data); + + ManualNotificationHandler1.WasInvoked = false; + await mediator.PublishAsync(new ManualNotification("n")); + Assert.True(ManualNotificationHandler1.WasInvoked); + } +} + +public sealed record ManualVoidCommand(string Value) : ICommand; + +public sealed record ManualCommand(string Value) : ICommand; + +public sealed record ManualQuery(int Id) : IQuery; + +public sealed record ManualNotification(string Payload) : INotification; + +public sealed record ManualResponse(string Data); + +public sealed class ManualVoidCommandHandler : ICommandHandler +{ + public static bool WasInvoked { get; set; } + + public ValueTask HandleAsync(ManualVoidCommand command, CancellationToken cancellationToken) + { + WasInvoked = true; + return default; + } +} + +public sealed class ManualCommandHandler : ICommandHandler +{ + public ValueTask HandleAsync(ManualCommand command, CancellationToken cancellationToken) + => new(new ManualResponse("manual-" + command.Value)); +} + +public sealed class ManualQueryHandler : IQueryHandler +{ + public ValueTask HandleAsync(ManualQuery query, CancellationToken cancellationToken) + => new(new ManualResponse("query-" + query.Id)); +} + +public sealed class ManualNotificationHandler1 : INotificationHandler +{ + public static bool WasInvoked { get; set; } + + public ValueTask HandleAsync(ManualNotification notification, CancellationToken cancellationToken) + { + WasInvoked = true; + return default; + } +} + +public sealed class ManualNotificationHandler2 : INotificationHandler +{ + public static bool WasInvoked { get; set; } + + public ValueTask HandleAsync(ManualNotification notification, CancellationToken cancellationToken) + { + WasInvoked = true; + return default; + } +} diff --git a/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs index 530581303e8..b525908b9be 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/ContextPoolingTests.cs @@ -11,17 +11,10 @@ public ContextPoolingTests() var services = new ServiceCollection(); var builder = services.AddMediator(); - // Void command - services.AddScoped, PoolTestCommandHandler>(); - - // Command with response - services.AddScoped, PoolTestCommandWithResponseHandler>(); - - // Nested-dispatch command: the handler dispatches another command internally - services.AddScoped, NestedOuterCommandHandler>(); - - // Context-capturing command - services.AddScoped, ContextCaptureCommandHandler>(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Register middleware that captures context references for nested-dispatch test // and context-field verification test. Since Use() applies to all pipelines, @@ -59,28 +52,10 @@ public ContextPoolingTests() builder.ConfigureMediator(b => { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PoolTestCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PoolTestCommandWithResponse), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(NestedOuterCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(ContextCaptureCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - }); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); }); _provider = services.BuildServiceProvider(); diff --git a/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs index c87f58a8808..a51b8413ced 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/InstrumentationTests.cs @@ -12,26 +12,17 @@ public InstrumentationTests() var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, InstrumentedCommandHandler>(); - services.AddScoped, InstrumentedThrowingCommandHandler>(); + services.AddScoped(); + services.AddScoped(); - var listener = _listener; builder.ConfigureMediator(b => { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(InstrumentedCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(InstrumentedThrowingCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); + b.AddHandler(); + b.AddHandler(); }); // Register test listener via the builder's internal services. - builder.AddDiagnosticEventListener(listener); + builder.AddDiagnosticEventListener(_listener); _provider = services.BuildServiceProvider(); } @@ -83,16 +74,9 @@ public async Task SendAsync_Should_InvokeAllListeners_When_MultipleListenersRegi var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, InstrumentedCommandHandler>(); + services.AddScoped(); - builder.ConfigureMediator(b => - { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(InstrumentedCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); - }); + builder.ConfigureMediator(b => b.AddHandler()); builder.AddDiagnosticEventListener(first); builder.AddDiagnosticEventListener(second); diff --git a/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs index d54ac19634d..22903842d54 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewareFactoryContextTests.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Mocha.Mediator; namespace Mocha.Mediator.Tests; @@ -333,8 +332,8 @@ public async Task Use_Should_SkipMiddleware_When_MessageTypeDoesNotMatch() var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, PipelineTestCommandHandler>(); - services.AddScoped, CtxTestVoidCommandHandler>(); + services.AddScoped(); + services.AddScoped(); // Middleware that only applies to void commands builder.Use(new MediatorMiddlewareConfiguration( @@ -353,17 +352,8 @@ public async Task Use_Should_SkipMiddleware_When_MessageTypeDoesNotMatch() builder.ConfigureMediator(b => { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CtxTestVoidCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - }); + b.AddHandler(); + b.AddHandler(); }); await using var provider = services.BuildServiceProvider(); @@ -392,8 +382,8 @@ public async Task Use_Should_SkipMiddleware_When_ResponseTypeDoesNotMatch() var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, PipelineTestCommandHandler>(); - services.AddScoped, CtxTestQueryHandler>(); + services.AddScoped(); + services.AddScoped(); // Middleware that only applies when response is int builder.Use(new MediatorMiddlewareConfiguration( @@ -412,18 +402,8 @@ public async Task Use_Should_SkipMiddleware_When_ResponseTypeDoesNotMatch() builder.ConfigureMediator(b => { - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - }); - b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CtxTestQuery), - ResponseType = typeof(int), - Terminal = PipelineBuilder.BuildQueryTerminal() - }); + b.AddHandler(); + b.AddHandler(); }); await using var provider = services.BuildServiceProvider(); @@ -483,45 +463,26 @@ private static IMediator BuildMediator( if (registerCommand) { - services.AddScoped, PipelineTestCommandHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + services.AddScoped(); + builder.ConfigureMediator(b => b.AddHandler()); } if (registerVoidCommand) { - services.AddScoped, CtxTestVoidCommandHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CtxTestVoidCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - })); + services.AddScoped(); + builder.ConfigureMediator(b => b.AddHandler()); } if (registerQuery) { - services.AddScoped, CtxTestQueryHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CtxTestQuery), - ResponseType = typeof(int), - Terminal = PipelineBuilder.BuildQueryTerminal() - })); + services.AddScoped(); + builder.ConfigureMediator(b => b.AddHandler()); } if (registerNotification) { services.AddScoped(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(CtxTestNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - [typeof(CtxTestNotificationHandler)]) - })); + builder.ConfigureMediator(b => b.AddHandler()); } var provider = services.BuildServiceProvider(); diff --git a/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs index a357147c4be..23cc1d9c452 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/MiddlewarePipelineTests.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Mocha.Mediator; namespace Mocha.Mediator.Tests; @@ -13,14 +12,9 @@ public MiddlewarePipelineTests() var builder = services.AddMediator(); - services.AddScoped, PipelineTestCommandHandler>(); + services.AddScoped(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); _provider = services.BuildServiceProvider(); } @@ -34,7 +28,7 @@ public async Task SendAsync_Should_ExecuteMiddlewareInRegistrationOrder_When_Mul var builder = services.AddMediator(); - services.AddScoped, PipelineTestCommandHandler>(); + services.AddScoped(); builder .Use(new MediatorMiddlewareConfiguration( @@ -98,12 +92,7 @@ static async ValueTask Awaited(ValueTask t, List l) }, "MW3")); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -130,7 +119,7 @@ public async Task SendAsync_Should_ExposeContextProperties_When_MiddlewareReadsC var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, PipelineTestCommandHandler>(); + services.AddScoped(); builder.Use(new MediatorMiddlewareConfiguration( (factoryCtx, next) => ctx => @@ -142,12 +131,7 @@ public async Task SendAsync_Should_ExposeContextProperties_When_MiddlewareReadsC }, "ContextReader")); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -172,7 +156,7 @@ public async Task SendAsync_Should_ReturnModifiedResult_When_MiddlewareModifiesR var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, PipelineTestCommandHandler>(); + services.AddScoped(); builder.Use(new MediatorMiddlewareConfiguration( (factoryCtx, next) => ctx => @@ -194,12 +178,7 @@ static async ValueTask Awaited(ValueTask t, IMediatorContext c) }, "ResultModifier")); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -221,7 +200,7 @@ public async Task SendAsync_Should_PropagateExceptionThroughMiddleware_When_Hand var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped, PipelineThrowingHandler>(); + services.AddScoped(); builder.Use(new MediatorMiddlewareConfiguration( (factoryCtx, next) => async ctx => @@ -238,12 +217,7 @@ public async Task SendAsync_Should_PropagateExceptionThroughMiddleware_When_Hand }, "ExceptionObserver")); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -265,7 +239,7 @@ public async Task SendAsync_Should_PropagateExceptionAndSkipHandler_When_Middlew var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped>( + services.AddScoped( _ => new PipelineTrackingHandler(() => handlerInvoked = true)); builder.Use(new MediatorMiddlewareConfiguration( @@ -273,12 +247,7 @@ public async Task SendAsync_Should_PropagateExceptionAndSkipHandler_When_Middlew throw new ApplicationException("middleware failure"), "FailingMiddleware")); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -301,7 +270,7 @@ public async Task SendAsync_Should_ReturnShortCircuitResult_When_MiddlewareSkips var services = new ServiceCollection(); var builder = services.AddMediator(); - services.AddScoped>( + services.AddScoped( _ => new PipelineTrackingHandler(() => handlerInvoked = true)); builder.Use(new MediatorMiddlewareConfiguration( @@ -312,12 +281,7 @@ public async Task SendAsync_Should_ReturnShortCircuitResult_When_MiddlewareSkips }, "ShortCircuit")); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(PipelineTestCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + builder.ConfigureMediator(b => b.AddHandler()); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); diff --git a/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs index 3b9d51e609f..2de0f56bad1 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/NamedMediatorTests.cs @@ -12,33 +12,18 @@ public NamedMediatorTests() // Default (unnamed) mediator with its own pipeline var defaultBuilder = services.AddMediator(); - services.AddScoped, DefaultCommandHandler>(); - defaultBuilder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DefaultCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + services.AddScoped(); + defaultBuilder.ConfigureMediator(b => b.AddHandler()); // Named mediator "billing" with its own pipeline var billingBuilder = services.AddMediator("billing"); - services.AddScoped, BillingCommandHandler>(); - billingBuilder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(BillingCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + services.AddScoped(); + billingBuilder.ConfigureMediator(b => b.AddHandler()); // Named mediator "shipping" with its own pipeline var shippingBuilder = services.AddMediator("shipping"); - services.AddScoped, ShippingCommandHandler>(); - shippingBuilder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(ShippingCommand), - ResponseType = typeof(string), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); + services.AddScoped(); + shippingBuilder.ConfigureMediator(b => b.AddHandler()); _provider = services.BuildServiceProvider(); } diff --git a/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs index 7cedd96efbc..d9e422f9c49 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using Microsoft.Extensions.DependencyInjection; -using Mocha.Mediator; namespace Mocha.Mediator.Tests; @@ -22,12 +21,12 @@ public async Task PublishAsync_Should_InvokeHandlersSequentially_When_UsingForea services.AddScoped( _ => new SequentialHandler3(log)); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + builder.ConfigureMediator(b => { - MessageType = typeof(StrategyTestNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - new[] { typeof(SequentialHandler1), typeof(SequentialHandler2), typeof(SequentialHandler3) }) - })); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + }); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -58,12 +57,12 @@ public async Task PublishAsync_Should_StopExecution_When_ForeachAwaitHandlerThro services.AddScoped( _ => new SequentialHandler3(log)); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + builder.ConfigureMediator(b => { - MessageType = typeof(StrategyTestNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - new[] { typeof(SequentialHandler1), typeof(StrategyThrowingHandler), typeof(SequentialHandler3) }) - })); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + }); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -85,12 +84,10 @@ public async Task PublishAsync_Should_InvokeAllHandlers_When_UsingTaskWhenAll() var bag = new ConcurrentBag(); var services = new ServiceCollection(); - - // Override default strategy with TaskWhenAllPublisher before AddMediator - services.AddSingleton(); - var builder = services.AddMediator(); + builder.ConfigureOptions(o => o.NotificationPublishMode = NotificationPublishMode.Concurrent); + services.AddScoped( _ => new ConcurrentHandler1(bag)); services.AddScoped( @@ -98,12 +95,12 @@ public async Task PublishAsync_Should_InvokeAllHandlers_When_UsingTaskWhenAll() services.AddScoped( _ => new ConcurrentHandler3(bag)); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + builder.ConfigureMediator(b => { - MessageType = typeof(StrategyTestNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - new[] { typeof(ConcurrentHandler1), typeof(ConcurrentHandler2), typeof(ConcurrentHandler3) }) - })); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + }); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); @@ -127,31 +124,31 @@ public async Task PublishAsync_Should_PropagateException_When_TaskWhenAllHandler var bag = new ConcurrentBag(); var services = new ServiceCollection(); - services.AddSingleton(); - var builder = services.AddMediator(); + builder.ConfigureOptions(o => o.NotificationPublishMode = NotificationPublishMode.Concurrent); + services.AddScoped( _ => new ConcurrentHandler1(bag)); services.AddScoped( _ => new StrategyThrowingHandler()); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration + builder.ConfigureMediator(b => { - MessageType = typeof(StrategyTestNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - new[] { typeof(ConcurrentHandler1), typeof(StrategyThrowingHandler) }) - })); + b.AddHandler(); + b.AddHandler(); + }); await using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); - // Act & Assert - var ex = await Assert.ThrowsAsync( + // Act & Assert — concurrent mode surfaces AggregateException with all failures + var ex = await Assert.ThrowsAsync( () => mediator.PublishAsync(new StrategyTestNotification("throw")).AsTask()); - Assert.Equal("notification handler error", ex.Message); + Assert.Single(ex.InnerExceptions); + Assert.Equal("notification handler error", ex.InnerExceptions[0].Message); } } diff --git a/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs b/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs index efe270f1b10..fdf4a8a05ea 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/TestSetup.cs @@ -2,10 +2,66 @@ namespace Mocha.Mediator.Tests; -// --------------------------------------------------------------------------- -// Message types (prefixed with "Dispatch" to avoid collisions with -// MiddlewarePipelineTests types) -// --------------------------------------------------------------------------- +public static class DispatchTestHelper +{ + /// + /// Creates a service provider with the mediator infrastructure, registering + /// only the handlers and pipelines specified by the caller. + /// + public static IServiceProvider BuildProvider(Action configure) + { + var services = new ServiceCollection(); + var builder = MediatorServiceCollectionExtensions.AddMediator(services); + configure(builder, services); + return services.BuildServiceProvider(); + } + + /// + /// Creates a service provider with the standard set of test handlers and pipelines. + /// + public static IServiceProvider BuildDefaultProvider() + { + return BuildProvider((builder, services) => + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + builder.ConfigureMediator(b => + { + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + b.AddHandler(); + }); + }); + } + + /// + /// Creates a service provider with multiple notification handlers to test fan-out dispatch. + /// + public static IServiceProvider BuildMultiNotificationProvider() + { + return BuildProvider((builder, services) => + { + services.AddScoped(); + services.AddScoped(); + + builder.ConfigureMediator(b => + { + b.AddHandler(); + b.AddHandler(); + }); + }); + } +} public sealed record DispatchVoidCommand(string Value) : ICommand; @@ -17,28 +73,12 @@ public sealed record DispatchNotification(string Payload) : INotification; public sealed record DispatchResponse(string Data); -// --------------------------------------------------------------------------- -// Command that always throws -// --------------------------------------------------------------------------- - public sealed record DispatchThrowingCommand(string Value) : ICommand; -// --------------------------------------------------------------------------- -// Command that captures the CancellationToken -// --------------------------------------------------------------------------- - public sealed record DispatchTokenCapturingCommand : ICommand; -// --------------------------------------------------------------------------- -// Async (non-synchronously completing) command -// --------------------------------------------------------------------------- - public sealed record DispatchAsyncCommand(string Value) : ICommand; -// --------------------------------------------------------------------------- -// Handlers -// --------------------------------------------------------------------------- - public sealed class DispatchVoidCommandHandler : ICommandHandler { public static bool WasInvoked { get; set; } @@ -113,110 +153,3 @@ public async ValueTask HandleAsync(DispatchAsyncCommand comman return new DispatchResponse("async-result"); } } - -// --------------------------------------------------------------------------- -// Helper to build a fully-wired IServiceProvider -// --------------------------------------------------------------------------- - -public static class DispatchTestHelper -{ - /// - /// Creates a service provider with the mediator infrastructure, registering - /// only the handlers and pipelines specified by the caller. - /// - public static IServiceProvider BuildProvider(Action configure) - { - var services = new ServiceCollection(); - var builder = MediatorServiceCollectionExtensions.AddMediator(services); - configure(builder, services); - return services.BuildServiceProvider(); - } - - /// - /// Creates a service provider with the standard set of test handlers and pipelines. - /// - public static IServiceProvider BuildDefaultProvider() - { - return BuildProvider((builder, services) => - { - // Void command - services.AddScoped, DispatchVoidCommandHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchVoidCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - })); - - // Command with response - services.AddScoped, DispatchCommandHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchCommand), - ResponseType = typeof(DispatchResponse), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); - - // Query - services.AddScoped, DispatchQueryHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchQuery), - ResponseType = typeof(DispatchResponse), - Terminal = PipelineBuilder.BuildQueryTerminal() - })); - - // Notification - single handler - services.AddScoped(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - new[] { typeof(DispatchNotificationHandler) }) - })); - - // Throwing command - services.AddScoped, DispatchThrowingCommandHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchThrowingCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - })); - - // Token capturing command - services.AddScoped, DispatchTokenCapturingHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchTokenCapturingCommand), - Terminal = PipelineBuilder.BuildVoidCommandTerminal() - })); - - // Async command - services.AddScoped, DispatchAsyncCommandHandler>(); - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchAsyncCommand), - ResponseType = typeof(DispatchResponse), - Terminal = PipelineBuilder.BuildCommandTerminal() - })); - }); - } - - /// - /// Creates a service provider with multiple notification handlers to test fan-out dispatch. - /// - public static IServiceProvider BuildMultiNotificationProvider() - { - return BuildProvider((builder, services) => - { - services.AddScoped(); - services.AddScoped(); - - builder.ConfigureMediator(b => b.RegisterPipeline(new MediatorPipelineConfiguration - { - MessageType = typeof(DispatchNotification), - Terminal = PipelineBuilder.BuildNotificationTerminal( - new[] { typeof(DispatchNotificationHandler), typeof(DispatchSecondNotificationHandler) }) - })); - }); - } -} diff --git a/website/src/docs/mocha/v1/mediator/index.md b/website/src/docs/mocha/v1/mediator/index.md index 3d742be0cac..2184e3fbb95 100644 --- a/website/src/docs/mocha/v1/mediator/index.md +++ b/website/src/docs/mocha/v1/mediator/index.md @@ -243,12 +243,12 @@ builder.Services .AddCatalog(); // source-generated from assembly name "Demo.Catalog" ``` -`AddMediator()` registers the core mediator infrastructure: the `Mediator` class, context pooling, and the default notification strategy. The source-generated `Add{ModuleName}()` method registers: +`AddMediator()` registers the core mediator infrastructure and the default notification publish mode. The source-generated `Add{ModuleName}()` method registers: - All command, query, and notification handlers found in your assembly -- Pipeline configurations with pre-compiled terminal delegates for each message type +- Pre-compiled terminal delegates for each message type (no reflection at runtime) -You do not register handlers manually. The source generator discovers them by scanning for classes that implement handler interfaces. +You do not register handlers manually unless you need to. The source generator discovers them by scanning for classes that implement handler interfaces. ## Module naming @@ -265,6 +265,29 @@ using Mocha.Mediator; [assembly: MediatorModule("Billing")] ``` +## Manual handler registration with AddHandler + +When you need to register handlers outside the source generator's reach - from a plugin assembly, a dynamically loaded module, or in integration tests - use `AddHandler()`: + +```csharp +builder.Services + .AddMediator() + .AddHandler() + .AddHandler() + .AddHandler(); +``` + +`AddHandler()` inspects the type for handler interfaces (`ICommandHandler`, `IQueryHandler`, `INotificationHandler`), builds the pipeline configuration, and registers the handler in DI. It throws `InvalidOperationException` if `T` does not implement a handler interface. + +You can mix source-generated and manual registration: + +```csharp +builder.Services + .AddMediator() + .AddCatalog() // source-generated handlers + .AddHandler(); // additional handler from another assembly +``` + ## Configure service lifetime By default, handlers are registered as `Scoped`. To change the default: @@ -281,9 +304,16 @@ builder.Services Call `ConfigureOptions` before `Add{ModuleName}()` so the source-generated method reads the updated lifetime. +## Configuration options reference + +| Option | Type | Default | Description | +| ------------------------- | ------------------------- | ------------ | --------------------------------------------- | +| `ServiceLifetime` | `ServiceLifetime` | `Scoped` | Default DI lifetime for handler registrations | +| `NotificationPublishMode` | `NotificationPublishMode` | `Sequential` | How notification handlers are dispatched | + # Named mediators -To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's keyed dependency injection. +To run multiple independent mediator instances (each with its own handlers and middleware), use named mediators. Named mediators use .NET's [keyed dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services). ```csharp // Register a named mediator @@ -436,7 +466,7 @@ The source generator did not find a handler for your message type. Verify: If dispatch succeeds but your handler code does not execute, check that: -- Your middleware calls the `next` delegate - a middleware that forgets to call `next` silently short-circuits the pipeline +- Your middleware calls the `next` delegate -- a middleware that forgets to call `next` silently short-circuits the pipeline - You are not accidentally registering handlers manually in addition to the source-generated method, which could result in duplicate registrations ## The source-generated method does not appear @@ -444,15 +474,19 @@ If dispatch succeeds but your handler code does not execute, check that: If IntelliSense does not show `Add{ModuleName}()`: - Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj` -- Rebuild the project - source generators run during compilation +- Rebuild the project -- source generators run during compilation - Check the build output for analyzer warnings prefixed with `MO` +## `InvalidOperationException` when calling `AddHandler()` + +The type you passed does not implement any handler interface. Make sure `T` implements one of: `ICommandHandler`, `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`. + ## Named mediator returns wrong handlers -Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct `IMediatorHostBuilder` instance - the one returned by the `AddMediator("name")` call for that name. +Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct `IMediatorHostBuilder` instance -- the one returned by the `AddMediator("name")` call for that name. # Next steps You have a working mediator with CQRS dispatch. Here is where to go next: -- **Customize the pipeline:** [Pipeline & Middleware](/docs/mocha/v1/mediator/pipeline-and-middleware) - add validation, logging, transactions, and other cross-cutting concerns using `MediatorMiddleware` and `MediatorMiddlewareConfiguration`. +- **Customize the pipeline:** [Pipeline & Middleware](/docs/mocha/v1/mediator/pipeline-and-middleware) -- add validation, logging, transactions, and other cross-cutting concerns. Configure notification publish modes and OpenTelemetry instrumentation. diff --git a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md index 88cdead9156..3d5fa14265a 100644 --- a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md +++ b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md @@ -1,6 +1,6 @@ --- title: "Pipeline & Middleware" -description: "Add cross-cutting concerns to the Mocha Mediator dispatch pipeline. Write middleware for logging, validation, transactions, and exception handling. Use compile-time filtering to eliminate middleware from pipelines where it does not apply. Configure notification strategies, Entity Framework Core transactions, and OpenTelemetry instrumentation." +description: "Add cross-cutting concerns to the Mocha Mediator dispatch pipeline. Write middleware for logging, validation, transactions, and exception handling. Configure notification publish modes and OpenTelemetry instrumentation." --- ```csharp @@ -73,7 +73,19 @@ public delegate MediatorDelegate MediatorMiddleware( MediatorDelegate next); ``` -At startup, the mediator iterates every registered middleware in reverse order. Each factory receives the `next` delegate and returns a new delegate that wraps it. The result is a single compiled `MediatorDelegate` per message type, stored in a frozen dictionary. At runtime, dispatch is a dictionary lookup followed by a single delegate invocation - no reflection, no generic resolution. +At startup, the mediator iterates every registered middleware in reverse order. Each factory receives the `next` delegate and returns a new delegate that wraps it. The result is a single compiled `MediatorDelegate` per message type. At runtime, dispatch is a direct delegate invocation - no reflection, no generic resolution. + +```mermaid +graph LR + A[SendAsync] --> B[Middleware 1] + B --> C[Middleware 2] + C --> D[Middleware N] + D --> E[Handler] + E -.-> D + D -.-> C + C -.-> B + B -.-> A +``` # Write a middleware @@ -130,16 +142,16 @@ builder.Services The `IMediatorContext` available at runtime provides everything you need during dispatch: -| Property | Type | Description | -| ------------------- | -------------------- | ----------------------------------------------------------------------- | -| `Message` | `object` | The message instance being dispatched | -| `MessageType` | `Type` | Runtime type of the message | -| `ResponseType` | `Type` | Expected response type (`Unit` for void commands and notifications) | -| `Result` | `object?` | The handler's return value, readable by middleware after calling `next` | -| `Services` | `IServiceProvider` | Scoped service provider for the current request | -| `CancellationToken` | `CancellationToken` | Cancellation token for the operation | -| `Features` | `IFeatureCollection` | Per-request feature collection for sharing state between middleware | -| `Runtime` | `IMediatorRuntime` | The mediator runtime that owns this context | +| Property | Type | Description | +| ------------------- | -------------------- | --------------------------------------------------------------------------- | +| `Message` | `object` | The message instance being dispatched | +| `MessageType` | `Type` | Runtime type of the message | +| `ResponseType` | `Type` | Expected response type (`typeof(void)` for void commands and notifications) | +| `Result` | `object?` | The handler's return value, readable by middleware after calling `next` | +| `Services` | `IServiceProvider` | Scoped service provider for the current request | +| `CancellationToken` | `CancellationToken` | Cancellation token for the operation | +| `Features` | `IFeatureCollection` | Per-request feature collection for sharing state between middleware | +| `Runtime` | `IMediatorRuntime` | The mediator runtime that owns this context | ## Short-circuiting @@ -334,11 +346,11 @@ Both approaches combine well - filter out entire message kinds at compile time, The `Use` method accepts optional `before` and `after` parameters to control where the middleware sits in the pipeline. -| Call | Behavior | -| ------------------------------------------- | --------------------------------------------------------- | -| `Use(config)` | Appends to the end of the middleware list | -| `Use(config, before: "Logging")` | Inserts before the middleware with key `"Logging"` | -| `Use(config, after: "Instrumentation")` | Inserts after the middleware with key `"Instrumentation"` | +| Call | Behavior | +| --------------------------------------- | --------------------------------------------------------- | +| `Use(config)` | Appends to the end of the middleware list | +| `Use(config, before: "Logging")` | Inserts before the middleware with key `"Logging"` | +| `Use(config, after: "Instrumentation")` | Inserts after the middleware with key `"Instrumentation"` | Only one of `before` or `after` can be specified at the same time. If the referenced key is not found, an `InvalidOperationException` is thrown at startup. @@ -361,7 +373,7 @@ The `Key` property on `MediatorMiddlewareConfiguration` is optional. Middleware | Key | Middleware | Added by | | ------------------------------ | -------------------------------------- | ------------------------------------------------------- | -| `"Instrumentation"` | `MediatorDiagnosticMiddleware` | Always present (added by `MediatorBuilder` constructor) | +| `"Instrumentation"` | `MediatorDiagnosticMiddleware` | Always present (added automatically by `AddMediator()`) | | `"EntityFrameworkTransaction"` | `EntityFrameworkTransactionMiddleware` | `UseEntityFrameworkTransactions()` | # Pipeline execution order @@ -382,56 +394,58 @@ Instrumentation <- outermost (runs first) Instrumentation returns <- runs last on the way out ``` -The `Instrumentation` middleware is always present as the first entry because `MediatorBuilder` adds it in its constructor. Your middleware registered via `Use()` follows in the order you call it. +The `Instrumentation` middleware is always present as the first entry because `AddMediator()` adds it automatically. Your middleware registered via `Use()` follows in the order you call it. -# Notification strategies +# Notification publish modes -When a notification has multiple handlers, the **notification strategy** controls how they are invoked. Mocha ships two strategies: +When a notification has multiple handlers, the **notification publish mode** controls how they are invoked. Configure it via `MediatorOptions`: -| Strategy | Behavior | Default | -| ----------------------- | ----------------------------------------------------- | ------- | -| `ForeachAwaitPublisher` | Invokes handlers one at a time, sequentially | Yes | -| `TaskWhenAllPublisher` | Invokes all handlers concurrently with `Task.WhenAll` | No | +```csharp +builder.Services + .AddMediator() + .ConfigureOptions(o => o.NotificationPublishMode = NotificationPublishMode.Sequential) + .AddCatalog(); +``` -The default `ForeachAwaitPublisher` guarantees ordering - handlers execute in registration order. If a handler throws, subsequent handlers do not execute. +| Mode | Behavior | Default | +| ------------ | -------------------------------------------------------------- | ------- | +| `Sequential` | Invokes handler pipelines one at a time, in registration order | Yes | +| `Concurrent` | Invokes all handler pipelines concurrently with `Task.WhenAll` | No | -To switch to concurrent execution: +## Sequential mode (default) -```csharp -builder.Services.AddSingleton(); -``` +Handlers execute one after another. If a handler throws, subsequent handlers do not execute and the exception propagates to the caller. -With `TaskWhenAllPublisher`, all handlers run concurrently. If any handler throws, the aggregate exception propagates after all handlers complete. +This is the right choice when handlers have ordering dependencies or when you want fail-fast behavior. -## Custom notification strategies +## Concurrent mode -Implement `INotificationStrategy` to control dispatch behavior: +All handlers execute in parallel. If any handler throws, the remaining handlers still run to completion and all exceptions are collected into an `AggregateException`. ```csharp -public class FireAndForgetPublisher : INotificationStrategy -{ - public ValueTask PublishAsync( - IReadOnlyList> handlers, - TNotification notification, - CancellationToken cancellationToken) - where TNotification : INotification - { - for (var i = 0; i < handlers.Count; i++) - { - _ = handlers[i].HandleAsync(notification, cancellationToken); - } - - return ValueTask.CompletedTask; - } -} +builder.Services + .AddMediator() + .ConfigureOptions(o => o.NotificationPublishMode = NotificationPublishMode.Concurrent) + .AddCatalog(); ``` -Register it as a singleton: +> **Warning:** In concurrent mode, all handler pipelines share the same scoped `IServiceProvider`. Scoped services such as `DbContext` are not thread-safe and must not be used concurrently across handlers. If your notification handlers need scoped services, use `Sequential` mode or create a new scope inside each handler. -```csharp -builder.Services.AddSingleton(); +## Per-handler middleware pipelines + +Each notification handler gets its own independently compiled middleware pipeline. When middleware is registered on the mediator, it wraps each notification handler individually - not the notification dispatch as a whole. + +```text +PublishAsync(OrderPlacedNotification) + ├── Pipeline for SendOrderConfirmationEmail: + │ Instrumentation -> Logging -> SendOrderConfirmationEmail + │ + └── Pipeline for UpdateAnalyticsDashboard: + Instrumentation -> Logging -> UpdateAnalyticsDashboard ``` +This means middleware like logging or exception handling runs independently around each handler. If middleware around one handler modifies `ctx.Result` or catches an exception, it does not affect the other handler's pipeline. + # Entity Framework Core transactions The `Mocha.EntityFrameworkCore` package provides middleware that wraps command handlers in a database transaction. Install the package and call `UseEntityFrameworkTransactions`: @@ -507,7 +521,7 @@ builder.Services.AddOpenTelemetry() To add your own instrumentation alongside or instead of the built-in listener, extend `MediatorDiagnosticEventListener`: ```csharp -public class SlowMessageListener : MediatorDiagnosticEventListener +public sealed class SlowMessageListener : MediatorDiagnosticEventListener { public override IDisposable Execute( Type messageType, Type responseType, object message) @@ -568,11 +582,19 @@ The `MediatorDiagnosticMiddleware` is always present, but it uses a no-op listen Services resolved from `factoryCtx.Services` in the middleware factory are resolved once at startup from the mediator's internal service provider. Use this for singletons like `ILoggerFactory`. To resolve scoped services (like `DbContext`), use `ctx.Services` inside the runtime delegate instead. +## Notification handler throws but other handlers do not run + +In `Sequential` mode (the default), the first handler exception stops execution of subsequent handlers. If you need all handlers to run regardless of failures, switch to `Concurrent` mode. In `Concurrent` mode, all handlers run to completion and exceptions are aggregated into an `AggregateException`. + +## Scoped service exceptions in concurrent notifications + +When using `NotificationPublishMode.Concurrent`, all handler pipelines execute in parallel but share the same scoped `IServiceProvider`. Scoped services like `DbContext` are not thread-safe. You will see race conditions or `ObjectDisposedException` if multiple handlers access the same scoped service concurrently. Switch to `Sequential` mode or create a new `IServiceScope` inside handlers that need their own scoped services. + > **Full demo:** The [Demo application](https://github.com/ChilliCream/graphql-platform/tree/main/src/Mocha/src/Demo) uses `UseEntityFrameworkTransactions` and `AddInstrumentation` alongside the mediator and message bus in a complete e-commerce system. # Next steps -- **Mediator overview:** [Overview](/docs/mocha/v1/mediator)messages, handlers, dispatching, and registration. -- **Message bus middleware:** [Middleware & Pipelines](/docs/mocha/v1/middleware-and-pipelines)the message bus has its own three-layer pipeline (dispatch, receive, consume) using the same middleware model. -- **Cross service boundaries:** [Messaging Patterns](/docs/mocha/v1/messaging-patterns) -when your commands need to reach another service, switch to the message bus. -- **Coordinate workflows:** [Sagas](/docs/mocha/v1/sagas)orchestrate multi-step processes across services. +- **Mediator overview:** [Overview](/docs/mocha/v1/mediator) - messages, handlers, dispatching, and registration. +- **Message bus middleware:** [Middleware & Pipelines](/docs/mocha/v1/middleware-and-pipelines) - the message bus has its own three-layer pipeline (dispatch, receive, consume) using the same middleware model. +- **Cross service boundaries:** [Messaging Patterns](/docs/mocha/v1/messaging-patterns) - when your commands need to reach another service, switch to the message bus. +- **Coordinate workflows:** [Sagas](/docs/mocha/v1/sagas) - orchestrate multi-step processes across services. From a1c5af89b6db6cd3205f423884e16d744727298d Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Thu, 26 Mar 2026 07:54:23 +0000 Subject: [PATCH 2/4] cleanup --- .../DependencyInjection/MediatorBuilder.cs | 27 +-- .../Descriptors/MediatorHandlerDescriptor.cs | 3 +- .../src/Mocha.Mediator/MediatorRuntime.cs | 22 +-- src/Mocha/src/Mocha.Mediator/ThrowHelper.cs | 29 +++ .../Builder/MessageBusBuilder.Middlewares.cs | 9 +- .../src/Mocha/Builder/MessageBusBuilder.cs | 5 +- .../RootServiceProviderAccessorExtensions.cs | 2 +- src/Mocha/src/Mocha/Consumers/Consumer.cs | 8 +- .../Descriptors/ConsumerDescriptor.cs | 3 +- .../Implementations/ReplyConsumer.cs | 4 +- .../Implementations/RequestConsumer.cs | 2 +- .../src/Mocha/Context/ConsumeContext~1.cs | 2 +- .../src/Mocha/DeferredResponseManager.cs | 2 +- .../MessageBusDescriptionBuilder.cs | 2 +- .../Descriptors/DispatchEndpointDescriptor.cs | 3 +- .../Descriptors/ReceiveEndpointDescriptor.cs | 3 +- .../src/Mocha/Endpoints/DispatchEndpoint.cs | 4 +- .../src/Mocha/Endpoints/EndpointRouter.cs | 4 +- .../src/Mocha/Endpoints/ReceiveEndpoint.cs | 4 +- .../MessageFeatureContextExtensions.cs | 11 +- .../src/Mocha/MessageTypes/IMessageRouter.cs | 6 +- .../src/Mocha/MessageTypes/InboundRoute.cs | 12 +- .../src/Mocha/MessageTypes/OutboundRoute.cs | 10 +- .../Mocha/Middlewares/DefaultMessageBus.cs | 7 +- .../Dispatch/DispatchSerializerMiddleware.cs | 12 +- .../MiddlewareConfigurationExtensions.cs | 12 +- .../CircuitBreaker/CircuitBreakerFeature.cs | 2 +- .../ConcurrencyLimiterFeature.cs | 2 +- .../Receive/ReceiveFaultMiddleware.cs | 2 +- .../Mocha/Naming/DefaultNamingConventions.cs | 2 +- .../src/Mocha/Runtime/LazyMessagingRuntime.cs | 3 +- .../Sagas/Descriptors/SagaStateDescriptor.cs | 6 +- src/Mocha/src/Mocha/Sagas/Saga.cs | 4 +- src/Mocha/src/Mocha/ThrowHelper.cs | 166 ++++++++++++++++++ .../src/Mocha/Topology/TopologyResource~1.cs | 2 +- .../Transport/MessagingTransport.Lifecyle.cs | 10 +- .../src/Mocha/Transport/MessagingTransport.cs | 10 +- .../Transport/MessagingTransportDescriptor.cs | 19 +- 38 files changed, 302 insertions(+), 134 deletions(-) create mode 100644 src/Mocha/src/Mocha.Mediator/ThrowHelper.cs create mode 100644 src/Mocha/src/Mocha/ThrowHelper.cs diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs index c9fe5781270..3b7c77db43d 100644 --- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using System.Collections.Immutable; using System.ComponentModel; using System.Reflection; using Microsoft.Extensions.DependencyInjection; @@ -53,8 +54,7 @@ public IMediatorBuilder Use(MediatorMiddlewareConfiguration middleware, string? if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) @@ -71,8 +71,7 @@ public IMediatorBuilder Use(MediatorMiddlewareConfiguration middleware, string? if (index == -1) { - throw new InvalidOperationException( - $"The middleware with the key `{anchor}` was not found."); + throw ThrowHelper.MiddlewareKeyNotFound(anchor); } pipeline.Insert(before is not null ? index : index + 1, middleware); @@ -116,7 +115,11 @@ public void AddHandler(Action? configure = if (existing is not null && configure is not null) { var inner = existing; - _handlerDescriptors[handlerType] = d => { inner(d); configure(d); }; + _handlerDescriptors[handlerType] = d => + { + inner(d); + configure(d); + }; } else if (configure is not null) { @@ -150,7 +153,11 @@ void ApplyConfig(MediatorHandlerDescriptor d) if (existing is not null) { var inner = existing; - _handlerDescriptors[handlerType] = d => { inner(d); ApplyConfig(d); }; + _handlerDescriptors[handlerType] = d => + { + inner(d); + ApplyConfig(d); + }; } else { @@ -236,21 +243,21 @@ public MediatorRuntime Build(IServiceProvider applicationServices) // Compile notification pipelines - each handler terminal is independently // wrapped in middleware, producing a MediatorDelegate[] per notification type. - var notificationPipelines = new Dictionary(notificationTerminals.Count); + var notificationPipelines = new Dictionary>(notificationTerminals.Count); foreach (var (notificationType, terminals) in notificationTerminals) { factoryCtx.MessageType = notificationType; factoryCtx.ResponseType = null; - var compiled = new MediatorDelegate[terminals.Count]; + var compiled = ImmutableArray.CreateBuilder(terminals.Count); for (var i = 0; i < terminals.Count; i++) { compiled[i] = MediatorMiddlewareCompiler.Compile( factoryCtx, terminals[i], middlewareConfigs, modifiers); } - notificationPipelines[notificationType] = compiled; + notificationPipelines[notificationType] = compiled.ToImmutable(); } var pools = applicationServices.GetRequiredService(); @@ -287,7 +294,7 @@ private static MediatorDelegate BuildPipelineViaReflection(MediatorHandlerConfig .MakeGenericMethod(config.HandlerType!, config.MessageType!) .Invoke(null, null)!, - _ => throw new InvalidOperationException($"Unknown handler kind: {config.Kind}") + _ => throw ThrowHelper.UnknownHandlerKind(config.Kind) }; } diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs index 1fbbdeab68b..c9827ce2f0b 100644 --- a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs @@ -85,7 +85,6 @@ private void DetectHandler(Type handlerType) } } - throw new InvalidOperationException( - $"Type '{handlerType}' does not implement any known mediator handler interface."); + throw ThrowHelper.HandlerInterfaceNotFound(handlerType); } } diff --git a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs index 84038dcdfc8..51966cab27b 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorRuntime.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using System.Collections.Immutable; using System.Runtime.CompilerServices; using Microsoft.Extensions.ObjectPool; @@ -11,7 +12,7 @@ namespace Mocha.Mediator; public sealed class MediatorRuntime : IMediatorRuntime { private readonly FrozenDictionary _pipelines; - private readonly FrozenDictionary _notificationPipelines; + private readonly FrozenDictionary> _notificationPipelines; private readonly ObjectPool _contextPool; [ThreadStatic] @@ -19,7 +20,7 @@ public sealed class MediatorRuntime : IMediatorRuntime internal MediatorRuntime( FrozenDictionary pipelines, - FrozenDictionary notificationPipelines, + FrozenDictionary> notificationPipelines, IMediatorPools pools, IFeatureCollection features, NotificationPublishMode notificationPublishMode) @@ -52,7 +53,7 @@ public MediatorDelegate GetPipeline(Type messageType) return pipeline; } - return ThrowMissingPipeline(messageType); + throw ThrowHelper.MissingPipeline(messageType); } /// @@ -94,24 +95,13 @@ public void ReturnContext(MediatorContext context) /// Gets the compiled notification pipeline delegates for the specified notification type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public MediatorDelegate[] GetNotificationPipelines(Type notificationType) + public ImmutableArray GetNotificationPipelines(Type notificationType) { if (_notificationPipelines.TryGetValue(notificationType, out var pipelines)) { return pipelines; } - return ThrowMissingNotificationPipeline(notificationType); + throw ThrowHelper.MissingNotificationPipeline(notificationType); } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static MediatorDelegate ThrowMissingPipeline(Type messageType) - => throw new InvalidOperationException( - $"No pipeline registered for message type {messageType}"); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static MediatorDelegate[] ThrowMissingNotificationPipeline(Type notificationType) - => throw new InvalidOperationException( - $"No notification pipeline registered for message type {notificationType}. " - + "If this is a command or query, use SendAsync or QueryAsync instead."); } diff --git a/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs b/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs new file mode 100644 index 00000000000..e4cf6b0c423 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs @@ -0,0 +1,29 @@ +namespace Mocha.Mediator; + +internal static class ThrowHelper +{ + public static Exception MissingPipeline(Type messageType) + => new InvalidOperationException( + $"No pipeline registered for message type {messageType}"); + + public static Exception MissingNotificationPipeline(Type notificationType) + => new InvalidOperationException( + $"No notification pipeline registered for message type {notificationType}. " + + "If this is a command or query, use SendAsync or QueryAsync instead."); + + public static Exception BeforeAndAfterConflict() + => new ArgumentException( + "Only one of 'before' or 'after' can be specified at the same time."); + + public static Exception MiddlewareKeyNotFound(string key) + => new InvalidOperationException( + $"The middleware with the key `{key}` was not found."); + + public static Exception UnknownHandlerKind(MediatorHandlerKind kind) + => new InvalidOperationException( + $"Unknown handler kind: {kind}"); + + public static Exception HandlerInterfaceNotFound(Type handlerType) + => new InvalidOperationException( + $"Type '{handlerType}' does not implement any known mediator handler interface."); +} diff --git a/src/Mocha/src/Mocha/Builder/MessageBusBuilder.Middlewares.cs b/src/Mocha/src/Mocha/Builder/MessageBusBuilder.Middlewares.cs index d7db6eb8865..8d2ebbaa1f9 100644 --- a/src/Mocha/src/Mocha/Builder/MessageBusBuilder.Middlewares.cs +++ b/src/Mocha/src/Mocha/Builder/MessageBusBuilder.Middlewares.cs @@ -39,8 +39,7 @@ public IMessageBusBuilder UseConsume( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) @@ -90,8 +89,7 @@ public IMessageBusBuilder UseReceive( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) @@ -141,8 +139,7 @@ public IMessageBusBuilder UseDispatch( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) diff --git a/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs b/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs index ad824da1cde..bb1949a5982 100644 --- a/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs +++ b/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs @@ -120,15 +120,14 @@ public IMessageBusBuilder AddHandler(Action confi } else { - throw new InvalidOperationException( - "Handler type must be either an event handler, a request handler, or both."); + throw ThrowHelper.InvalidHandlerType(); } var consumer = Activator.CreateInstance(consumerType, configure) as Consumer; if (consumer is null) { - throw new InvalidOperationException($"Failed to create consumer for type {consumerType}"); + throw ThrowHelper.FailedToCreateConsumer(consumerType); } _consumers.Add(consumer); diff --git a/src/Mocha/src/Mocha/Configuration/RootServiceProviderAccessorExtensions.cs b/src/Mocha/src/Mocha/Configuration/RootServiceProviderAccessorExtensions.cs index 7baa19db671..62a768213f0 100644 --- a/src/Mocha/src/Mocha/Configuration/RootServiceProviderAccessorExtensions.cs +++ b/src/Mocha/src/Mocha/Configuration/RootServiceProviderAccessorExtensions.cs @@ -20,6 +20,6 @@ public static class RootServiceProviderAccessorExtensions public static IServiceProvider GetApplicationServices(this IServiceProvider sp) { return sp.GetService()?.ServiceProvider - ?? throw new InvalidOperationException("No root services found"); + ?? throw ThrowHelper.NoRootServicesFound(); } } diff --git a/src/Mocha/src/Mocha/Consumers/Consumer.cs b/src/Mocha/src/Mocha/Consumers/Consumer.cs index 4bbd74bda87..9afc99cec40 100644 --- a/src/Mocha/src/Mocha/Consumers/Consumer.cs +++ b/src/Mocha/src/Mocha/Consumers/Consumer.cs @@ -88,7 +88,7 @@ public async ValueTask ProcessAsync(IReceiveContext context) { if (context is not IConsumeContext handlerContext) { - throw new InvalidOperationException("Context is not a handler context"); + throw ThrowHelper.InvalidHandlerContext(); } await _pipeline(handlerContext); @@ -123,11 +123,11 @@ internal void Initialize(IMessagingSetupContext context) if (Configuration is null) { - throw new InvalidOperationException("Handler configuration is null"); + throw ThrowHelper.HandlerConfigurationMissing(); } // TODO should we assign a default name in the Action? GetType().Name? - Name = Configuration.Name ?? throw new InvalidOperationException("Consumer name is null"); + Name = Configuration.Name ?? throw ThrowHelper.ConsumerNameRequired(); Identity ??= GetType(); foreach (var route in Configuration!.Routes) @@ -189,7 +189,7 @@ private void AssertUninitialized() { if (_isInitialized) { - throw new InvalidOperationException("Handler already initialized"); + throw ThrowHelper.HandlerAlreadyInitialized(); } } diff --git a/src/Mocha/src/Mocha/Consumers/Descriptors/ConsumerDescriptor.cs b/src/Mocha/src/Mocha/Consumers/Descriptors/ConsumerDescriptor.cs index 4bbba6566e8..eaec0e93f81 100644 --- a/src/Mocha/src/Mocha/Consumers/Descriptors/ConsumerDescriptor.cs +++ b/src/Mocha/src/Mocha/Consumers/Descriptors/ConsumerDescriptor.cs @@ -45,8 +45,7 @@ public IConsumerDescriptor UseConsumer( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) diff --git a/src/Mocha/src/Mocha/Consumers/Implementations/ReplyConsumer.cs b/src/Mocha/src/Mocha/Consumers/Implementations/ReplyConsumer.cs index a3868bc002b..0bef89c71c8 100644 --- a/src/Mocha/src/Mocha/Consumers/Implementations/ReplyConsumer.cs +++ b/src/Mocha/src/Mocha/Consumers/Implementations/ReplyConsumer.cs @@ -39,7 +39,7 @@ protected override ValueTask ConsumeAsync(IConsumeContext context) if (message is null) { - throw new InvalidOperationException("Response body is not set. Could not be parsed."); + throw ThrowHelper.ResponseBodyNotSet(); } if (message is NotAcknowledgedEvent failure) @@ -56,7 +56,7 @@ protected override ValueTask ConsumeAsync(IConsumeContext context) else if (!responseManager.CompletePromise(context.CorrelationId, message)) { // A late/unknown reply indicates there is no active waiter for this correlation id. - throw new InvalidOperationException("Promise with correlation ID not found."); + throw ThrowHelper.PromiseNotFound(); } } catch (Exception ex) diff --git a/src/Mocha/src/Mocha/Consumers/Implementations/RequestConsumer.cs b/src/Mocha/src/Mocha/Consumers/Implementations/RequestConsumer.cs index f3242a7bfea..68484751a55 100644 --- a/src/Mocha/src/Mocha/Consumers/Implementations/RequestConsumer.cs +++ b/src/Mocha/src/Mocha/Consumers/Implementations/RequestConsumer.cs @@ -51,7 +51,7 @@ protected override async ValueTask ConsumeAsync(IConsumeContext context) // Request contracts require a response message; null would break caller expectations. if (response is null) { - throw new InvalidOperationException("Response is null."); + throw ThrowHelper.ResponseIsNull(); } // Copy request metadata (correlation/saga-related headers) onto the reply path. diff --git a/src/Mocha/src/Mocha/Context/ConsumeContext~1.cs b/src/Mocha/src/Mocha/Context/ConsumeContext~1.cs index 85a351c7547..8549247f7b8 100644 --- a/src/Mocha/src/Mocha/Context/ConsumeContext~1.cs +++ b/src/Mocha/src/Mocha/Context/ConsumeContext~1.cs @@ -15,7 +15,7 @@ public ConsumeContext(IConsumeContext inner) public TMessage Message => field ??= - Inner.GetMessage() ?? throw new InvalidOperationException("Could not deserialize message"); + Inner.GetMessage() ?? throw ThrowHelper.CouldNotDeserializeMessage(); public IFeatureCollection Features => Inner.Features; diff --git a/src/Mocha/src/Mocha/DeferredResponseManager.cs b/src/Mocha/src/Mocha/DeferredResponseManager.cs index fbcebbe0cbd..e3fafebf3c9 100644 --- a/src/Mocha/src/Mocha/DeferredResponseManager.cs +++ b/src/Mocha/src/Mocha/DeferredResponseManager.cs @@ -50,7 +50,7 @@ public sealed class DeferredResponseManager(TimeProvider timeProvider) return await promise.TaskCompletionSource.Task; } - throw new InvalidOperationException("Promise not found."); + throw ThrowHelper.PromiseNotFound(); } /// diff --git a/src/Mocha/src/Mocha/Descriptions/MessageBusDescriptionBuilder.cs b/src/Mocha/src/Mocha/Descriptions/MessageBusDescriptionBuilder.cs index be853e3e128..b0ab03152bc 100644 --- a/src/Mocha/src/Mocha/Descriptions/MessageBusDescriptionBuilder.cs +++ b/src/Mocha/src/Mocha/Descriptions/MessageBusDescriptionBuilder.cs @@ -34,7 +34,7 @@ public sealed class Context internal MessageBusDescription ToDescription() => new( - Host ?? throw new InvalidOperationException("Host description is missing."), + Host ?? throw ThrowHelper.HostDescriptionMissing(), MessageTypes, Consumers, new RoutesDescription(InboundRoutes, OutboundRoutes), diff --git a/src/Mocha/src/Mocha/Endpoints/Descriptors/DispatchEndpointDescriptor.cs b/src/Mocha/src/Mocha/Endpoints/Descriptors/DispatchEndpointDescriptor.cs index 554fffd47f7..d3084bbffd7 100644 --- a/src/Mocha/src/Mocha/Endpoints/Descriptors/DispatchEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha/Endpoints/Descriptors/DispatchEndpointDescriptor.cs @@ -29,8 +29,7 @@ public IDispatchEndpointDescriptor UseDispatch( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) diff --git a/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs index fcb113eb08c..16fb62c78d7 100644 --- a/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs @@ -53,8 +53,7 @@ public IReceiveEndpointDescriptor UseReceive( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) diff --git a/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs b/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs index ba721e075b3..b0176c455c2 100644 --- a/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs +++ b/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs @@ -131,7 +131,7 @@ public void Initialize(IMessagingConfigurationContext context, DispatchEndpointC Transport.Conventions.Configure(context, Transport, configuration); Configuration = configuration; Kind = configuration.Kind; - Name = configuration.Name ?? throw new InvalidOperationException("Name is required"); + Name = configuration.Name ?? throw ThrowHelper.EndpointNameRequired(); OnInitialize(context, configuration); @@ -250,7 +250,7 @@ private void AssertUninitialized() if (IsInitialized) { - throw new InvalidOperationException("Endpoint already initialized"); + throw ThrowHelper.EndpointAlreadyInitialized(); } } diff --git a/src/Mocha/src/Mocha/Endpoints/EndpointRouter.cs b/src/Mocha/src/Mocha/Endpoints/EndpointRouter.cs index e2e715f3bb2..58a3c699420 100644 --- a/src/Mocha/src/Mocha/Endpoints/EndpointRouter.cs +++ b/src/Mocha/src/Mocha/Endpoints/EndpointRouter.cs @@ -109,7 +109,7 @@ public DispatchEndpoint GetOrCreate(IMessagingConfigurationContext context, Uri } } - throw new InvalidOperationException($"No transport can handle address: {address}"); + throw ThrowHelper.NoTransportForAddress(address.ToString()); } } @@ -134,7 +134,7 @@ public void AddAddress(DispatchEndpoint endpoint, Uri address) { if (!_endpoints.TryGetValue(endpoint, out var addresses)) { - throw new InvalidOperationException("Endpoint must be registered before adding addresses"); + throw ThrowHelper.EndpointMustBeRegistered(); } if (addresses.Contains(address)) diff --git a/src/Mocha/src/Mocha/Endpoints/ReceiveEndpoint.cs b/src/Mocha/src/Mocha/Endpoints/ReceiveEndpoint.cs index 173f1a33812..686ba65f586 100644 --- a/src/Mocha/src/Mocha/Endpoints/ReceiveEndpoint.cs +++ b/src/Mocha/src/Mocha/Endpoints/ReceiveEndpoint.cs @@ -167,7 +167,7 @@ public void Initialize(IMessagingConfigurationContext context, ReceiveEndpointCo Transport.Conventions.Configure(context, Transport, configuration); Configuration = configuration; Kind = configuration.Kind; - Name = configuration.Name ?? throw new InvalidOperationException("Name is required"); + Name = configuration.Name ?? throw ThrowHelper.EndpointNameRequired(); configuration.Features.CopyTo(Features); OnInitialize(context, Configuration); @@ -324,7 +324,7 @@ private void AssertUninitialized() if (IsInitialized) { - throw new InvalidOperationException("Endpoint already initialized"); + throw ThrowHelper.EndpointAlreadyInitialized(); } } diff --git a/src/Mocha/src/Mocha/Features/MessageFeatureContextExtensions.cs b/src/Mocha/src/Mocha/Features/MessageFeatureContextExtensions.cs index ef983630f1b..384e35f1829 100644 --- a/src/Mocha/src/Mocha/Features/MessageFeatureContextExtensions.cs +++ b/src/Mocha/src/Mocha/Features/MessageFeatureContextExtensions.cs @@ -26,7 +26,7 @@ public static class MessageFeatureContextExtensions if (context.Envelope is null) { - throw new InvalidOperationException("Envelope is required for deserialization"); + throw ThrowHelper.EnvelopeRequired(); } var serializer = context.GetSerializer(); @@ -56,7 +56,7 @@ public static class MessageFeatureContextExtensions if (context.Envelope is null) { - throw new InvalidOperationException("Envelope is required for deserialization"); + throw ThrowHelper.EnvelopeRequired(); } var serializer = context.GetSerializer(); @@ -71,20 +71,19 @@ private static IMessageSerializer GetSerializer(this IMessageContext context) { if (context.MessageType is null) { - throw new InvalidOperationException("Message type is required for deserialization"); + throw ThrowHelper.MessageTypeRequired(); } if (context.ContentType is null) { - throw new InvalidOperationException("Content type is required for deserialization"); + throw ThrowHelper.ContentTypeRequired(); } var serializer = context.MessageType.GetSerializer(context.ContentType); if (serializer is null) { - throw new InvalidOperationException( - $"No serializer was found for message type {context.MessageType.Identity} and content type {context.ContentType}"); + throw ThrowHelper.SerializerNotFound(context.MessageType.Identity, context.ContentType.Value); } return serializer; diff --git a/src/Mocha/src/Mocha/MessageTypes/IMessageRouter.cs b/src/Mocha/src/Mocha/MessageTypes/IMessageRouter.cs index e4b4c5745a4..7225168b1ba 100644 --- a/src/Mocha/src/Mocha/MessageTypes/IMessageRouter.cs +++ b/src/Mocha/src/Mocha/MessageTypes/IMessageRouter.cs @@ -180,7 +180,7 @@ public DispatchEndpoint GetEndpoint( } route.Complete(context); - throw new InvalidOperationException($"No transport can handle message type: {messageType}"); + throw ThrowHelper.NoTransportForMessageType(messageType); } } @@ -190,7 +190,7 @@ public void AddOrUpdate(InboundRoute route) if (!route.IsInitialized) { - throw new InvalidOperationException("Route must be initialized"); + throw ThrowHelper.RouteMustBeInitialized(); } lock (_lock) @@ -240,7 +240,7 @@ public void AddOrUpdate(OutboundRoute route) ArgumentNullException.ThrowIfNull(route); if (!route.IsInitialized) { - throw new InvalidOperationException("Route must be initialized"); + throw ThrowHelper.RouteMustBeInitialized(); } lock (_lock) diff --git a/src/Mocha/src/Mocha/MessageTypes/InboundRoute.cs b/src/Mocha/src/Mocha/MessageTypes/InboundRoute.cs index 0dce018d2da..9c4d8154d6b 100644 --- a/src/Mocha/src/Mocha/MessageTypes/InboundRoute.cs +++ b/src/Mocha/src/Mocha/MessageTypes/InboundRoute.cs @@ -60,10 +60,10 @@ public void Initialize(IMessagingConfigurationContext context, InboundRouteConfi } else if (configuration.Kind != InboundRouteKind.Reply) { - throw new InvalidOperationException("Route requires a message type"); + throw ThrowHelper.RouteRequiresMessageType(); } - Consumer = configuration.Consumer ?? throw new InvalidOperationException("Route requires a consumer"); + Consumer = configuration.Consumer ?? throw ThrowHelper.RouteRequiresConsumer(); Kind = configuration.Kind; if (configuration.ResponseRuntimeType is not null) @@ -99,7 +99,7 @@ public void Complete(IMessagingConfigurationContext context) if (Endpoint is null) { - throw new InvalidOperationException("Endpoint is not connected"); + throw ThrowHelper.RouteEndpointNotConnected(); } MarkCompleted(); @@ -109,7 +109,7 @@ private void AssertNotInitialized() { if (IsInitialized) { - throw new InvalidOperationException("Route must not be initialized"); + throw ThrowHelper.RouteMustNotBeInitialized(); } } @@ -117,7 +117,7 @@ private void AssertInitialized() { if (!IsInitialized) { - throw new InvalidOperationException("Rout must be initialized"); + throw ThrowHelper.RouteMustBeInitialized(); } } @@ -125,7 +125,7 @@ private void AssertNotCompleted() { if (IsCompleted) { - throw new InvalidOperationException("Route must not be completed"); + throw ThrowHelper.RouteMustNotBeCompleted(); } } diff --git a/src/Mocha/src/Mocha/MessageTypes/OutboundRoute.cs b/src/Mocha/src/Mocha/MessageTypes/OutboundRoute.cs index 4df49170a95..ca39b990319 100644 --- a/src/Mocha/src/Mocha/MessageTypes/OutboundRoute.cs +++ b/src/Mocha/src/Mocha/MessageTypes/OutboundRoute.cs @@ -58,7 +58,7 @@ public void Initialize(IMessagingConfigurationContext context, OutboundRouteConf } else { - throw new InvalidOperationException("Cannot initialize outbound route without a message type"); + throw ThrowHelper.RouteRequiresMessageType(); } Destination = configuration.Destination; @@ -92,7 +92,7 @@ public void Complete(IMessagingConfigurationContext context) if (Endpoint is null) { - throw new InvalidOperationException("Endpoint is not connected"); + throw ThrowHelper.RouteEndpointNotConnected(); } context.Router.AddOrUpdate(this); @@ -104,7 +104,7 @@ private void AssertNotInitialized() { if (IsInitialized) { - throw new InvalidOperationException("Route must not be initialized"); + throw ThrowHelper.RouteMustNotBeInitialized(); } } @@ -112,7 +112,7 @@ private void AssertInitialized() { if (!IsInitialized) { - throw new InvalidOperationException("Route must be initialized"); + throw ThrowHelper.RouteMustBeInitialized(); } } @@ -120,7 +120,7 @@ private void AssertNotCompleted() { if (IsCompleted) { - throw new InvalidOperationException("Route must not be completed"); + throw ThrowHelper.RouteMustNotBeCompleted(); } } diff --git a/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs b/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs index f66adea7802..b4e72a62cff 100644 --- a/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs +++ b/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs @@ -206,14 +206,13 @@ public async ValueTask ReplyAsync( var transport = runtime.GetTransport(options.ReplyAddress); if (transport is null) { - throw new InvalidOperationException($"Transport not found for address {options.ReplyAddress}"); + throw ThrowHelper.TransportNotFoundForAddress(options.ReplyAddress.ToString()); } var replyEndpoint = transport.ReplyDispatchEndpoint; if (replyEndpoint is null) { - throw new InvalidOperationException( - $"Reply dispatch endpoint not found for address {options.ReplyAddress}"); + throw ThrowHelper.ReplyDispatchEndpointNotFound(options.ReplyAddress.ToString()); } // var operationName = "reply"; @@ -293,7 +292,7 @@ private async ValueTask RequestAndWaitAsync( return response; } - throw new InvalidOperationException("Unexpected response type."); + throw ThrowHelper.UnexpectedResponseType(); } private void PropagateCorrelationIds(DispatchContext context) diff --git a/src/Mocha/src/Mocha/Middlewares/Dispatch/DispatchSerializerMiddleware.cs b/src/Mocha/src/Mocha/Middlewares/Dispatch/DispatchSerializerMiddleware.cs index b123fdad145..c2de9906a70 100644 --- a/src/Mocha/src/Mocha/Middlewares/Dispatch/DispatchSerializerMiddleware.cs +++ b/src/Mocha/src/Mocha/Middlewares/Dispatch/DispatchSerializerMiddleware.cs @@ -20,28 +20,24 @@ public async ValueTask InvokeAsync(IDispatchContext context, DispatchDelegate ne { if (context.Message is null) { - throw new InvalidOperationException( - "To send a message either the body must be set or the message must be set"); + throw ThrowHelper.DispatchMessageRequired(); } if (context.MessageType is null) { - throw new InvalidOperationException( - "To send a message a message type must be set. Otherwise there is no way to serialize the message"); + throw ThrowHelper.DispatchMessageTypeRequired(); } if (context.ContentType is null) { - throw new InvalidOperationException( - "To send a message a content type must be set. Otherwise there is no way to serialize the message"); + throw ThrowHelper.DispatchContentTypeRequired(); } var serializer = context.MessageType.GetSerializer(context.ContentType); if (serializer is null) { - throw new InvalidOperationException( - $"No serializer found for content type {context.ContentType.Value} and message type {context.MessageType.Identity}"); + throw ThrowHelper.DispatchSerializerNotFound(context.ContentType.Value, context.MessageType.Identity); } serializer.Serialize(context.Message, context.Body); diff --git a/src/Mocha/src/Mocha/Middlewares/Extensions/MiddlewareConfigurationExtensions.cs b/src/Mocha/src/Mocha/Middlewares/Extensions/MiddlewareConfigurationExtensions.cs index ab7dadfa13d..d9f0f1d3bff 100644 --- a/src/Mocha/src/Mocha/Middlewares/Extensions/MiddlewareConfigurationExtensions.cs +++ b/src/Mocha/src/Mocha/Middlewares/Extensions/MiddlewareConfigurationExtensions.cs @@ -29,7 +29,7 @@ public static void Append( var index = pipeline.FindIndex(m => m.Key == after); if (index == -1) { - throw new InvalidOperationException($"Middleware with key {after} not found"); + throw ThrowHelper.MiddlewareKeyNotFound(after); } pipeline.Insert(index + 1, configuration); @@ -58,7 +58,7 @@ public static void Prepend( var index = pipeline.FindIndex(m => m.Key == before); if (index == -1) { - throw new InvalidOperationException($"Middleware with key {before} not found"); + throw ThrowHelper.MiddlewareKeyNotFound(before); } pipeline.Insert(index, configuration); @@ -114,7 +114,7 @@ public void Append(ReceiveMiddlewareConfiguration configuration, string? after) var index = pipeline.FindIndex(m => m.Key == after); if (index == -1) { - throw new InvalidOperationException($"Middleware with key {after} not found"); + throw ThrowHelper.MiddlewareKeyNotFound(after); } pipeline.Insert(index + 1, configuration); @@ -139,7 +139,7 @@ public void Prepend(ReceiveMiddlewareConfiguration configuration, string? before var index = pipeline.FindIndex(m => m.Key == before); if (index == -1) { - throw new InvalidOperationException($"Middleware with key {before} not found"); + throw ThrowHelper.MiddlewareKeyNotFound(before); } pipeline.Insert(index, configuration); @@ -198,7 +198,7 @@ public void Append(ConsumerMiddlewareConfiguration configuration, string? after) var index = pipeline.FindIndex(m => m.Key == after); if (index == -1) { - throw new InvalidOperationException($"Middleware with key {after} not found"); + throw ThrowHelper.MiddlewareKeyNotFound(after); } pipeline.Insert(index + 1, configuration); @@ -223,7 +223,7 @@ public void Prepend(ConsumerMiddlewareConfiguration configuration, string? befor var index = pipeline.FindIndex(m => m.Key == before); if (index == -1) { - throw new InvalidOperationException($"Middleware with key {before} not found"); + throw ThrowHelper.MiddlewareKeyNotFound(before); } pipeline.Insert(index, configuration); diff --git a/src/Mocha/src/Mocha/Middlewares/Receive/CircuitBreaker/CircuitBreakerFeature.cs b/src/Mocha/src/Mocha/Middlewares/Receive/CircuitBreaker/CircuitBreakerFeature.cs index 2527e6fef66..c19aa83d237 100644 --- a/src/Mocha/src/Mocha/Middlewares/Receive/CircuitBreaker/CircuitBreakerFeature.cs +++ b/src/Mocha/src/Mocha/Middlewares/Receive/CircuitBreaker/CircuitBreakerFeature.cs @@ -52,7 +52,7 @@ public void Configure(Action configure) { if (IsReadOnly) { - throw new InvalidOperationException("The feature is read-only."); + throw ThrowHelper.FeatureIsReadOnly(); } configure(_options); diff --git a/src/Mocha/src/Mocha/Middlewares/Receive/ConcurrencyLimiter/ConcurrencyLimiterFeature.cs b/src/Mocha/src/Mocha/Middlewares/Receive/ConcurrencyLimiter/ConcurrencyLimiterFeature.cs index d4ce2662882..76117009e3d 100644 --- a/src/Mocha/src/Mocha/Middlewares/Receive/ConcurrencyLimiter/ConcurrencyLimiterFeature.cs +++ b/src/Mocha/src/Mocha/Middlewares/Receive/ConcurrencyLimiter/ConcurrencyLimiterFeature.cs @@ -37,7 +37,7 @@ public void Configure(Action configure) { if (IsReadOnly) { - throw new InvalidOperationException("The feature is read-only."); + throw ThrowHelper.FeatureIsReadOnly(); } configure(_options); diff --git a/src/Mocha/src/Mocha/Middlewares/Receive/ReceiveFaultMiddleware.cs b/src/Mocha/src/Mocha/Middlewares/Receive/ReceiveFaultMiddleware.cs index 619e0b867b8..434a8a14e4c 100644 --- a/src/Mocha/src/Mocha/Middlewares/Receive/ReceiveFaultMiddleware.cs +++ b/src/Mocha/src/Mocha/Middlewares/Receive/ReceiveFaultMiddleware.cs @@ -62,7 +62,7 @@ private async ValueTask ReplyToSenderAsync( if (replyEndpoint is null) { // TODO critical error! (Poision Pill) - throw new InvalidOperationException($"No reply endpoint was found for {replyEndpoint} "); + throw ThrowHelper.NoReplyEndpointFound(responseAddress.ToString()); } var messageType = context.Runtime.GetMessageType(typeof(NotAcknowledgedEvent)); diff --git a/src/Mocha/src/Mocha/Naming/DefaultNamingConventions.cs b/src/Mocha/src/Mocha/Naming/DefaultNamingConventions.cs index 09128eb70e6..1334d0eab73 100644 --- a/src/Mocha/src/Mocha/Naming/DefaultNamingConventions.cs +++ b/src/Mocha/src/Mocha/Naming/DefaultNamingConventions.cs @@ -25,7 +25,7 @@ public string GetReceiveEndpointName(InboundRoute route, ReceiveEndpointKind kin if (!route.IsInitialized) { - throw new InvalidOperationException("Route is not initialized"); + throw ThrowHelper.RouteNotInitialized(); } return route.Kind switch diff --git a/src/Mocha/src/Mocha/Runtime/LazyMessagingRuntime.cs b/src/Mocha/src/Mocha/Runtime/LazyMessagingRuntime.cs index f1397633148..44a5c2b08f6 100644 --- a/src/Mocha/src/Mocha/Runtime/LazyMessagingRuntime.cs +++ b/src/Mocha/src/Mocha/Runtime/LazyMessagingRuntime.cs @@ -8,8 +8,7 @@ public IMessagingRuntime Runtime { if (field is null) { - throw new InvalidOperationException( - "Messaging runtime is not initialized, you can only access the runtime after it has been built."); + throw ThrowHelper.MessagingRuntimeNotInitialized(); } return field; diff --git a/src/Mocha/src/Mocha/Sagas/Descriptors/SagaStateDescriptor.cs b/src/Mocha/src/Mocha/Sagas/Descriptors/SagaStateDescriptor.cs index 74d820f92c2..33d0930b0fd 100644 --- a/src/Mocha/src/Mocha/Sagas/Descriptors/SagaStateDescriptor.cs +++ b/src/Mocha/src/Mocha/Sagas/Descriptors/SagaStateDescriptor.cs @@ -34,8 +34,7 @@ public ISagaTransitionDescriptor OnEvent() where TEvent if (eventType.IsEventRequest()) { - throw new InvalidOperationException( - $"Event type '{eventType}' is a request and should be handled with 'OnRequest' method."); + throw ThrowHelper.SagaEventIsRequest(eventType); } return On(SagaTransitionKind.Event); @@ -60,8 +59,7 @@ public ISagaTransitionDescriptor OnReply() where TEvent if (eventType.IsEventRequest()) { - throw new InvalidOperationException( - $"Event type '{eventType}' is a request and should be handled with 'OnRequest' method."); + throw ThrowHelper.SagaEventIsRequest(eventType); } return On(SagaTransitionKind.Reply); diff --git a/src/Mocha/src/Mocha/Sagas/Saga.cs b/src/Mocha/src/Mocha/Sagas/Saga.cs index 5965a44546e..0d758839b99 100644 --- a/src/Mocha/src/Mocha/Sagas/Saga.cs +++ b/src/Mocha/src/Mocha/Sagas/Saga.cs @@ -214,7 +214,7 @@ protected Saga() /// /// Thrown when the saga has not been initialized. public override IReadOnlyDictionary States - => _states ?? throw new InvalidOperationException("Saga is not initialized."); + => _states ?? throw ThrowHelper.SagaNotInitialized(); /// public override Type StateType => typeof(TState); @@ -230,7 +230,7 @@ public override async Task HandleEvent(IConsumeContext context) { if (_states is null) { - throw new InvalidOperationException("Saga is not initialized."); + throw ThrowHelper.SagaNotInitialized(); } var ct = context.CancellationToken; diff --git a/src/Mocha/src/Mocha/ThrowHelper.cs b/src/Mocha/src/Mocha/ThrowHelper.cs new file mode 100644 index 00000000000..bb54e267fb9 --- /dev/null +++ b/src/Mocha/src/Mocha/ThrowHelper.cs @@ -0,0 +1,166 @@ +namespace Mocha; + +internal static class ThrowHelper +{ + public static Exception BeforeAndAfterConflict() + => new ArgumentException( + "Only one of 'before' or 'after' can be specified at the same time."); + + public static Exception MiddlewareKeyNotFound(string key) + => new InvalidOperationException($"Middleware with key {key} not found"); + + public static Exception RouteEndpointNotConnected() + => new InvalidOperationException("Endpoint is not connected"); + + public static Exception RouteMustNotBeInitialized() + => new InvalidOperationException("Route must not be initialized"); + + public static Exception RouteMustBeInitialized() + => new InvalidOperationException("Route must be initialized"); + + public static Exception RouteMustNotBeCompleted() + => new InvalidOperationException("Route must not be completed"); + + public static Exception RouteRequiresMessageType() + => new InvalidOperationException("Route requires a message type"); + + public static Exception RouteRequiresConsumer() + => new InvalidOperationException("Route requires a consumer"); + + public static Exception RouteNotInitialized() + => new InvalidOperationException("Route is not initialized"); + + public static Exception TransportConfigurationMissing() + => new InvalidOperationException("Could not create configuration for transport"); + + public static Exception TransportNameRequired() + => new InvalidOperationException("Transport name is required"); + + public static Exception TransportSchemaRequired() + => new InvalidOperationException("Transport schema is required"); + + public static Exception TransportAlreadyStarted() + => new InvalidOperationException("Transport is already started"); + + public static Exception TransportNotStarted() + => new InvalidOperationException("Transport is not started"); + + public static Exception EndpointConfigurationFailed() + => new InvalidOperationException("Failed to create endpoint configuration"); + + public static Exception ReplyConsumerNotFound() + => new InvalidOperationException("Reply consumer not found"); + + public static Exception FeaturesNotInitialized() + => new InvalidOperationException("Features are not initialized"); + + public static Exception ConsumerNameRequired() + => new InvalidOperationException("Consumer name is null"); + + public static Exception HandlerConfigurationMissing() + => new InvalidOperationException("Handler configuration is null"); + + public static Exception HandlerAlreadyInitialized() + => new InvalidOperationException("Handler already initialized"); + + public static Exception InvalidHandlerContext() + => new InvalidOperationException("Context is not a handler context"); + + public static Exception EndpointNameRequired() + => new InvalidOperationException("Name is required"); + + public static Exception EndpointAlreadyInitialized() + => new InvalidOperationException("Endpoint already initialized"); + + public static Exception NoTransportForAddress(string address) + => new InvalidOperationException($"No transport can handle address: {address}"); + + public static Exception EndpointMustBeRegistered() + => new InvalidOperationException("Endpoint must be registered before adding addresses"); + + public static Exception NoTransportForMessageType(MessageType messageType) + => new InvalidOperationException($"No transport can handle message type: {messageType}"); + + public static Exception TransportNotFoundForAddress(string address) + => new InvalidOperationException($"Transport not found for address {address}"); + + public static Exception ReplyDispatchEndpointNotFound(string address) + => new InvalidOperationException($"Reply dispatch endpoint not found for address {address}"); + + public static Exception NoReplyEndpointFound(string endpoint) + => new InvalidOperationException($"No reply endpoint was found for {endpoint}"); + + public static Exception UnexpectedResponseType() + => new InvalidOperationException("Unexpected response type."); + + public static Exception ResponseIsNull() + => new InvalidOperationException("Response is null."); + + public static Exception ResponseBodyNotSet() + => new InvalidOperationException("Response body is not set. Could not be parsed."); + + public static Exception PromiseNotFound() + => new InvalidOperationException("Promise with correlation ID not found."); + + public static Exception EnvelopeRequired() + => new InvalidOperationException("Envelope is required for deserialization"); + + public static Exception MessageTypeRequired() + => new InvalidOperationException("Message type is required for deserialization"); + + public static Exception ContentTypeRequired() + => new InvalidOperationException("Content type is required for deserialization"); + + public static Exception SerializerNotFound(string messageType, string contentType) + => new InvalidOperationException( + $"No serializer was found for message type {messageType} and content type {contentType}"); + + public static Exception CouldNotDeserializeMessage() + => new InvalidOperationException("Could not deserialize message"); + + public static Exception DispatchMessageRequired() + => new InvalidOperationException( + "To send a message either the body must be set or the message must be set"); + + public static Exception DispatchMessageTypeRequired() + => new InvalidOperationException( + "To send a message a message type must be set. Otherwise there is no way to serialize the message"); + + public static Exception DispatchContentTypeRequired() + => new InvalidOperationException( + "To send a message a content type must be set. Otherwise there is no way to serialize the message"); + + public static Exception DispatchSerializerNotFound(string contentType, string messageType) + => new InvalidOperationException( + $"No serializer found for content type {contentType} and message type {messageType}"); + + public static Exception TopologyRequired() + => new InvalidOperationException("Topology is required"); + + public static Exception MessagingRuntimeNotInitialized() + => new InvalidOperationException( + "Messaging runtime is not initialized, you can only access the runtime after it has been built."); + + public static Exception FailedToCreateConsumer(Type consumerType) + => new InvalidOperationException($"Failed to create consumer for type {consumerType}"); + + public static Exception InvalidHandlerType() + => new InvalidOperationException( + "Handler type must be either an event handler, a request handler, or both."); + + public static Exception NoRootServicesFound() + => new InvalidOperationException("No root services found"); + + public static Exception SagaNotInitialized() + => new InvalidOperationException("Saga is not initialized."); + + public static Exception SagaEventIsRequest(Type eventType) + => new InvalidOperationException( + $"Event type '{eventType}' is a request and should be handled with 'OnRequest' method."); + + public static Exception FeatureIsReadOnly() + => new InvalidOperationException("The feature is read-only."); + + public static Exception HostDescriptionMissing() + => new InvalidOperationException("Host description is missing."); +} diff --git a/src/Mocha/src/Mocha/Topology/TopologyResource~1.cs b/src/Mocha/src/Mocha/Topology/TopologyResource~1.cs index aef140654db..969bbf97d03 100644 --- a/src/Mocha/src/Mocha/Topology/TopologyResource~1.cs +++ b/src/Mocha/src/Mocha/Topology/TopologyResource~1.cs @@ -10,7 +10,7 @@ public abstract class TopologyResource : TopologyResource where T : TopologyC protected sealed override void OnInitialize(TopologyConfiguration configuration) { - Topology = configuration.Topology ?? throw new InvalidOperationException("Topology is required"); + Topology = configuration.Topology ?? throw ThrowHelper.TopologyRequired(); OnInitialize((T)configuration); } diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransport.Lifecyle.cs b/src/Mocha/src/Mocha/Transport/MessagingTransport.Lifecyle.cs index 76b29ee0527..af9480d1963 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransport.Lifecyle.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransport.Lifecyle.cs @@ -18,11 +18,11 @@ internal void Initialize(IMessagingSetupContext context) if (Configuration is null) { - throw new InvalidOperationException("Could not create configuration for transport"); + throw ThrowHelper.TransportConfigurationMissing(); } - Name = Configuration.Name ?? throw new InvalidOperationException("Transport name is required"); - Schema = Configuration.Schema ?? throw new InvalidOperationException("Transport schema is required"); + Name = Configuration.Name ?? throw ThrowHelper.TransportNameRequired(); + Schema = Configuration.Schema ?? throw ThrowHelper.TransportSchemaRequired(); Naming = context.Naming; Conventions = new ConventionRegistry(context.Conventions.Concat(Configuration.Conventions)); Options = Configuration.Options; @@ -148,7 +148,7 @@ internal void DiscoverEndpoints(IMessagingSetupContext context) if (replyConsumer is null) { - throw new InvalidOperationException("Reply consumer not found"); + throw ThrowHelper.ReplyConsumerNotFound(); } var route = new InboundRoute(); @@ -159,7 +159,7 @@ internal void DiscoverEndpoints(IMessagingSetupContext context) var endpointConfiguration = CreateEndpointConfiguration(context, route); if (endpointConfiguration is null) { - throw new InvalidOperationException("Failed to create endpoint configuration"); + throw ThrowHelper.EndpointConfigurationFailed(); } var endpoint = AddEndpoint(context, endpointConfiguration); diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransport.cs b/src/Mocha/src/Mocha/Transport/MessagingTransport.cs index 305269b7edb..6e2961c7e46 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransport.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransport.cs @@ -61,7 +61,7 @@ public abstract partial class MessagingTransport : IAsyncDisposable, IFeaturePro /// /// Thrown if accessed before the transport is initialized. public IFeatureCollection Features - => _features ?? throw new InvalidOperationException("Features are not initialized"); + => _features ?? throw ThrowHelper.FeaturesNotInitialized(); /// /// The messaging topology that describes the transport's addressing structure (exchanges, queues, topics). @@ -181,7 +181,7 @@ public async ValueTask StartAsync(IMessagingRuntimeContext context, Cancellation AssertInitialized(); if (IsStarted) { - throw new InvalidOperationException("Transport is already started"); + throw ThrowHelper.TransportAlreadyStarted(); } await OnBeforeStartAsync(context, cancellationToken); @@ -204,7 +204,7 @@ public async ValueTask StopAsync(IMessagingRuntimeContext context, CancellationT { if (!IsStarted) { - throw new InvalidOperationException("Transport is not started"); + throw ThrowHelper.TransportNotStarted(); } await OnBeforeStopAsync(cancellationToken); @@ -253,7 +253,7 @@ public DispatchEndpoint ConnectRoute(IMessagingConfigurationContext context, Out { if (CreateEndpointConfiguration(context, route) is not { } configuration) { - throw new InvalidOperationException("Failed to create endpoint configuration"); + throw ThrowHelper.EndpointConfigurationFailed(); } var endpoint = @@ -276,7 +276,7 @@ public ReceiveEndpoint ConnectRoute(IMessagingConfigurationContext context, Inbo { if (CreateEndpointConfiguration(context, route) is not { } configuration) { - throw new InvalidOperationException("Failed to create endpoint configuration"); + throw ThrowHelper.EndpointConfigurationFailed(); } var endpoint = diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs index 8f9a00e76d0..b39466c88f6 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs @@ -152,8 +152,7 @@ public IMessagingTransportDescriptor UseDispatch( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) @@ -189,8 +188,7 @@ public IMessagingTransportDescriptor UseReceive( { if (before is not null && after is not null) { - throw new ArgumentException( - "Only one of 'before' or 'after' can be specified at the same time."); + throw ThrowHelper.BeforeAndAfterConflict(); } if (before is null && after is null) @@ -229,6 +227,8 @@ public IMessagingTransportDescriptor UseReceive( public IMessagingDescriptorExtension ExtendWith( Action> configure) { + configure(this); + return this; } @@ -243,15 +243,8 @@ public IMessagingDescriptorExtension ExtendWith Action, TState> configure, TState state) { - return this; - } + configure(this, state); - /// - /// Marks this descriptor as internal, preventing external consumers from using it. - /// - /// Always thrown; this method is not yet implemented. - public void Internal() - { - throw new NotImplementedException(); + return this; } } From 2b5c71e64d667717a28ed42495cfa1cc71c74645 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Thu, 26 Mar 2026 20:57:48 +0000 Subject: [PATCH 3/4] cleanup --- .../AnalyzerReleases.Unshipped.md | 1 + src/Mocha/src/Mocha.Analyzers/Errors.cs | 16 ++++ .../MultipleHandlerInterfaceInspector.cs | 88 +++++++++++++++++++ .../src/Mocha.Analyzers/MediatorGenerator.cs | 4 +- .../Descriptors/IMediatorDescriptor.cs | 4 +- .../Descriptors/MediatorDescriptorBase.cs | 4 +- .../Descriptors/MediatorHandlerDescriptor.cs | 44 +++++++--- src/Mocha/src/Mocha.Mediator/Mediator.cs | 61 +++++++------ .../src/Mocha.Mediator/MediatorOptions.cs | 23 ----- .../Mocha.Mediator/NotificationPublishMode.cs | 24 +++++ src/Mocha/src/Mocha.Mediator/ThrowHelper.cs | 5 ++ .../Transport/MessagingTransportDescriptor.cs | 4 + .../Mocha.Analyzers.Tests/DiagnosticTests.cs | 75 ++++++++++++++++ ...mandAndNotificationHandler_ReportsError.md | 28 ++++++ ...005_CommandAndQueryHandler_ReportsError.md | 39 ++++++++ ...ing_SingleHandlerInterface_NoDiagnostic.md | 33 +++++++ .../Mocha.Mediator.Tests/AddHandlerTests.cs | 20 ++--- website/src/docs/mocha/v1/mediator/index.md | 8 +- 18 files changed, 403 insertions(+), 78 deletions(-) create mode 100644 src/Mocha/src/Mocha.Analyzers/Inspectors/MultipleHandlerInterfaceInspector.cs create mode 100644 src/Mocha/src/Mocha.Mediator/NotificationPublishMode.cs create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndNotificationHandler_ReportsError.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndQueryHandler_ReportsError.md create mode 100644 src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md diff --git a/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md index 8c98066f5de..5b5c6b3f8ec 100644 --- a/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md @@ -6,3 +6,4 @@ MO0001 | Mediator | Warning | Message type has no registered handler MO0002 | Mediator | Error | Message type has multiple handlers MO0003 | Mediator | Warning | Handler is abstract and will not be registered MO0004 | Mediator | Info | Open generic message type cannot be dispatched +MO0005 | Mediator | Error | Handler implements multiple mediator handler interfaces diff --git a/src/Mocha/src/Mocha.Analyzers/Errors.cs b/src/Mocha/src/Mocha.Analyzers/Errors.cs index c0231e3f398..84021f29a0f 100644 --- a/src/Mocha/src/Mocha.Analyzers/Errors.cs +++ b/src/Mocha/src/Mocha.Analyzers/Errors.cs @@ -67,4 +67,20 @@ public static class Errors category: "Mediator", defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true); + + /// + /// Gets the descriptor for MO0005: a handler type implements multiple mediator handler interfaces. + /// + /// + /// Reported as an error when a concrete class implements more than one of + /// ICommandHandler, IQueryHandler, or INotificationHandler. + /// A handler must implement exactly one mediator handler interface. + /// + public static readonly DiagnosticDescriptor MultipleHandlerInterfaces = new( + id: "MO0005", + title: "Handler implements multiple mediator handler interfaces", + messageFormat: "Handler '{0}' must implement exactly one mediator handler interface", + category: "Mediator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/MultipleHandlerInterfaceInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/MultipleHandlerInterfaceInspector.cs new file mode 100644 index 00000000000..02dbf9eda34 --- /dev/null +++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/MultipleHandlerInterfaceInspector.cs @@ -0,0 +1,88 @@ +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mocha.Analyzers.Filters; +using Mocha.Analyzers.Utils; + +namespace Mocha.Analyzers.Inspectors; + +public sealed class MultipleHandlerInterfaceInspector : ISyntaxInspector +{ + public ImmutableArray Filters { get; } = [ClassWithMochaBaseListFilter.Instance]; + + public IImmutableSet SupportedKinds { get; } = + ImmutableHashSet.Create(SyntaxKind.ClassDeclaration, SyntaxKind.RecordDeclaration); + + public bool TryHandle( + KnownTypeSymbols knownSymbols, + SyntaxNode node, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out SyntaxInfo? syntaxInfo) + { + if (node is not TypeDeclarationSyntax typeDeclaration) + { + syntaxInfo = null; + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration); + + if (typeSymbol is null + || typeSymbol.IsAbstract + || typeSymbol.TypeKind == TypeKind.Interface) + { + syntaxInfo = null; + return false; + } + + var count = 0; + + foreach (var @interface in typeSymbol.Interfaces) + { + var original = @interface.OriginalDefinition; + + if (SymbolEqualityComparer.Default.Equals(original, knownSymbols.ICommandHandlerVoid) + || SymbolEqualityComparer.Default.Equals(original, knownSymbols.ICommandHandlerResponse) + || SymbolEqualityComparer.Default.Equals(original, knownSymbols.IQueryHandler) + || SymbolEqualityComparer.Default.Equals(original, knownSymbols.INotificationHandler)) + { + count++; + + if (count > 1) + { + break; + } + } + } + + if (count <= 1) + { + syntaxInfo = null; + return false; + } + + var handlerName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var locationInfo = typeDeclaration.Identifier.GetLocation().ToLocationInfo(); + + syntaxInfo = new MultipleHandlerInterfaceDiagnosticInfo(handlerName) + { + Diagnostics = new([ + new DiagnosticInfo( + Errors.MultipleHandlerInterfaces.Id, + locationInfo, + new([handlerName])) + ]) + }; + return true; + } +} + +internal sealed record MultipleHandlerInterfaceDiagnosticInfo(string HandlerTypeName) : SyntaxInfo +{ + public override string OrderByKey => $"MultipleHandlerDiag:{HandlerTypeName}"; +} diff --git a/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs b/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs index c43a4576e4b..85d11bdfe62 100644 --- a/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs +++ b/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs @@ -21,6 +21,7 @@ public sealed class MediatorGenerator : IIncrementalGenerator { private static readonly ISyntaxInspector[] s_allInspectors = [ + new MultipleHandlerInterfaceInspector(), new HandlerInspector(), new NotificationHandlerInspector(), new MessageTypeInspector(), @@ -244,7 +245,8 @@ private static void ValidateMessageHandlerPairing( [Errors.MissingHandler.Id] = Errors.MissingHandler, [Errors.DuplicateHandler.Id] = Errors.DuplicateHandler, [Errors.AbstractHandler.Id] = Errors.AbstractHandler, - [Errors.OpenGenericMessageType.Id] = Errors.OpenGenericMessageType + [Errors.OpenGenericMessageType.Id] = Errors.OpenGenericMessageType, + [Errors.MultipleHandlerInterfaces.Id] = Errors.MultipleHandlerInterfaces }; private static Diagnostic ReconstructDiagnostic(DiagnosticInfo info) diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs index 4ce2c4f27e1..92775360095 100644 --- a/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/IMediatorDescriptor.cs @@ -19,7 +19,9 @@ public interface IMediatorDescriptor : IMediatorDescriptor where T : Medi /// /// Provides access to the underlying configuration. This is useful for extensions. /// - IMediatorDescriptorExtension ExtendWith(Action, TState> configure, TState state); + IMediatorDescriptorExtension ExtendWith( + Action, TState> configure, + TState state); } /// diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs index b49fc8e963c..4034966488d 100644 --- a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorDescriptorBase.cs @@ -29,7 +29,9 @@ public IMediatorDescriptorExtension ExtendWith(Action ExtendWith(Action, TState> configure, TState state) + public IMediatorDescriptorExtension ExtendWith( + Action, TState> configure, + TState state) { ArgumentNullException.ThrowIfNull(configure); diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs index c9827ce2f0b..1706e688ff4 100644 --- a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs +++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs @@ -43,6 +43,8 @@ public MediatorHandlerConfiguration CreateConfiguration() private void DetectHandler(Type handlerType) { + var found = false; + foreach (var @interface in handlerType.GetInterfaces()) { if (!@interface.IsGenericType) @@ -54,37 +56,57 @@ private void DetectHandler(Type handlerType) if (genericDef == typeof(ICommandHandler<,>)) { + if (found) + { + throw ThrowHelper.MultipleHandlerInterfaces(handlerType); + } + var args = @interface.GetGenericArguments(); Configuration.MessageType = args[0]; Configuration.ResponseType = args[1]; Configuration.Kind = MediatorHandlerKind.CommandResponse; - return; + found = true; } - - if (genericDef == typeof(ICommandHandler<>)) + else if (genericDef == typeof(ICommandHandler<>)) { + if (found) + { + throw ThrowHelper.MultipleHandlerInterfaces(handlerType); + } + Configuration.MessageType = @interface.GetGenericArguments()[0]; Configuration.Kind = MediatorHandlerKind.Command; - return; + found = true; } - - if (genericDef == typeof(IQueryHandler<,>)) + else if (genericDef == typeof(IQueryHandler<,>)) { + if (found) + { + throw ThrowHelper.MultipleHandlerInterfaces(handlerType); + } + var args = @interface.GetGenericArguments(); Configuration.MessageType = args[0]; Configuration.ResponseType = args[1]; Configuration.Kind = MediatorHandlerKind.Query; - return; + found = true; } - - if (genericDef == typeof(INotificationHandler<>)) + else if (genericDef == typeof(INotificationHandler<>)) { + if (found) + { + throw ThrowHelper.MultipleHandlerInterfaces(handlerType); + } + Configuration.MessageType = @interface.GetGenericArguments()[0]; Configuration.Kind = MediatorHandlerKind.Notification; - return; + found = true; } } - throw ThrowHelper.HandlerInterfaceNotFound(handlerType); + if (!found) + { + throw ThrowHelper.HandlerInterfaceNotFound(handlerType); + } } } diff --git a/src/Mocha/src/Mocha.Mediator/Mediator.cs b/src/Mocha/src/Mocha.Mediator/Mediator.cs index 050ce3f63f0..83676d466ce 100644 --- a/src/Mocha/src/Mocha.Mediator/Mediator.cs +++ b/src/Mocha/src/Mocha.Mediator/Mediator.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Runtime.CompilerServices; namespace Mocha.Mediator; @@ -168,7 +169,7 @@ private ValueTask PublishSingle( [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] private async ValueTask PublishSequentially( - MediatorDelegate[] pipelines, + ImmutableArray pipelines, object notification, Type messageType, CancellationToken cancellationToken) @@ -190,52 +191,58 @@ private async ValueTask PublishSequentially( [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] private async ValueTask PublishConcurrently( - MediatorDelegate[] pipelines, + ImmutableArray pipelines, object notification, Type messageType, CancellationToken cancellationToken) { var count = pipelines.Length; - var contexts = new MediatorContext[count]; var tasks = new Task[count]; for (var i = 0; i < count; i++) { - var context = runtime.RentContext(); - context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); - contexts[i] = context; - tasks[i] = pipelines[i](context).AsTask(); + tasks[i] = RunPipeline(pipelines[i], notification, messageType, cancellationToken); } + var whenAll = Task.WhenAll(tasks); + try { - var whenAll = Task.WhenAll(tasks); - - try - { - await whenAll.ConfigureAwait(false); - } - catch - { - // Task.WhenAll captures all exceptions, but await unwraps only the first. - // Re-throw the AggregateException to surface all failures. - if (whenAll.Exception is not null) - { - throw whenAll.Exception; - } - - throw; - } + await whenAll.ConfigureAwait(false); } - finally + catch { - for (var i = 0; i < count; i++) + // Task.WhenAll captures all exceptions, but await unwraps only the first. + // Re-throw the AggregateException to surface all failures. + if (whenAll.Exception is not null) { - runtime.ReturnContext(contexts[i]); + throw whenAll.Exception; } + + throw; } } + private Task RunPipeline( + MediatorDelegate pipeline, + object notification, + Type messageType, + CancellationToken cancellationToken) + { + var context = runtime.RentContext(); + context.Initialize(runtime, serviceProvider, notification, messageType, cancellationToken); + + var task = pipeline(context); + + if (task.IsCompletedSuccessfully) + { + runtime.ReturnContext(context); + return Task.CompletedTask; + } + + return AwaitAndReturn(task, context).AsTask(); + } + [MethodImpl(MethodImplOptions.NoInlining)] [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] private async ValueTask AwaitAndReturn(ValueTask task, MediatorContext context) diff --git a/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs b/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs index ebb9ca5a70b..9a7cedfa0f8 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorOptions.cs @@ -19,26 +19,3 @@ public sealed class MediatorOptions /// public NotificationPublishMode NotificationPublishMode { get; set; } = NotificationPublishMode.Sequential; } - -/// -/// Specifies how notification handler pipelines are dispatched. -/// -public enum NotificationPublishMode -{ - /// - /// Handlers are invoked sequentially, awaiting each before proceeding to the next. - /// If a handler throws, subsequent handlers are not invoked. - /// - Sequential, - - /// - /// Handlers are invoked concurrently using . - /// All handlers are started simultaneously and awaited together. - /// - /// Warning: All concurrent handler pipelines share the same scoped - /// . Scoped services such as DbContext - /// are not thread-safe and must not be used concurrently across handlers. - /// - /// - Concurrent -} diff --git a/src/Mocha/src/Mocha.Mediator/NotificationPublishMode.cs b/src/Mocha/src/Mocha.Mediator/NotificationPublishMode.cs new file mode 100644 index 00000000000..0f95bde5f32 --- /dev/null +++ b/src/Mocha/src/Mocha.Mediator/NotificationPublishMode.cs @@ -0,0 +1,24 @@ +namespace Mocha.Mediator; + +/// +/// Specifies how notification handler pipelines are dispatched. +/// +public enum NotificationPublishMode +{ + /// + /// Handlers are invoked sequentially, awaiting each before proceeding to the next. + /// If a handler throws, subsequent handlers are not invoked. + /// + Sequential, + + /// + /// Handlers are invoked concurrently using . + /// All handlers are started simultaneously and awaited together. + /// + /// Warning: All concurrent handler pipelines share the same scoped + /// . Scoped services such as DbContext + /// are not thread-safe and must not be used concurrently across handlers. + /// + /// + Concurrent +} diff --git a/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs b/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs index e4cf6b0c423..0c6af56845c 100644 --- a/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs +++ b/src/Mocha/src/Mocha.Mediator/ThrowHelper.cs @@ -26,4 +26,9 @@ public static Exception UnknownHandlerKind(MediatorHandlerKind kind) public static Exception HandlerInterfaceNotFound(Type handlerType) => new InvalidOperationException( $"Type '{handlerType}' does not implement any known mediator handler interface."); + + public static Exception MultipleHandlerInterfaces(Type handlerType) + => new InvalidOperationException( + $"Type '{handlerType}' implements multiple mediator handler interfaces. " + + "A handler must implement exactly one of ICommandHandler<>, IQueryHandler<,>, or INotificationHandler<>."); } diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs index b39466c88f6..3243dbbf7e7 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs @@ -227,6 +227,8 @@ public IMessagingTransportDescriptor UseReceive( public IMessagingDescriptorExtension ExtendWith( Action> configure) { + ArgumentNullException.ThrowIfNull(configure); + configure(this); return this; @@ -243,6 +245,8 @@ public IMessagingDescriptorExtension ExtendWith Action, TState> configure, TState state) { + ArgumentNullException.ThrowIfNull(configure); + configure(this, state); return this; diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs index 822745a5d4f..5f64289b575 100644 --- a/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs +++ b/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs @@ -168,4 +168,79 @@ public ValueTask HandleAsync(GenericQuery query, CancellationToken cancell """ ]).MatchMarkdownAsync(); } + + [Fact] + public async Task MO0005_CommandAndNotificationHandler_ReportsError() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DoSomethingCommand : ICommand; + public record SomethingHappened : INotification; + + public class MultiHandler + : ICommandHandler + , INotificationHandler + { + public ValueTask HandleAsync(DoSomethingCommand command, CancellationToken cancellationToken) + => default; + + public ValueTask HandleAsync(SomethingHappened notification, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task MO0005_CommandAndQueryHandler_ReportsError() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record DoSomethingCommand : ICommand; + public record GetSomethingQuery : IQuery; + + public class MultiHandler + : ICommandHandler + , IQueryHandler + { + public ValueTask HandleAsync(DoSomethingCommand command, CancellationToken cancellationToken) + => default; + + public ValueTask HandleAsync(GetSomethingQuery query, CancellationToken cancellationToken) + => new("result"); + } + """ + ]).MatchMarkdownAsync(); + } + + [Fact] + public async Task NoWarning_SingleHandlerInterface_NoDiagnostic() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using Mocha.Mediator; + + namespace TestApp; + + public record SomethingHappened : INotification; + + public class SomethingHappenedHandler : INotificationHandler + { + public ValueTask HandleAsync(SomethingHappened notification, CancellationToken cancellationToken) + => default; + } + """ + ]).MatchMarkdownAsync(); + } } diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndNotificationHandler_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndNotificationHandler_ReportsError.md new file mode 100644 index 00000000000..dc0172c7eb1 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndNotificationHandler_ReportsError.md @@ -0,0 +1,28 @@ +# MO0005_CommandAndNotificationHandler_ReportsError + +```json +[ + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (4,14)-(4,32)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.DoSomethingCommand' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + }, + { + "Id": "MO0005", + "Title": "Handler implements multiple mediator handler interfaces", + "Severity": "Error", + "WarningLevel": 0, + "Location": ": (7,13)-(7,25)", + "MessageFormat": "Handler '{0}' must implement exactly one mediator handler interface", + "Message": "Handler 'global::TestApp.MultiHandler' must implement exactly one mediator handler interface", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndQueryHandler_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndQueryHandler_ReportsError.md new file mode 100644 index 00000000000..08197a0eb74 --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0005_CommandAndQueryHandler_ReportsError.md @@ -0,0 +1,39 @@ +# MO0005_CommandAndQueryHandler_ReportsError + +```json +[ + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (4,14)-(4,32)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.DoSomethingCommand' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + }, + { + "Id": "MO0001", + "Title": "Missing handler for message type", + "Severity": "Warning", + "WarningLevel": 1, + "Location": ": (5,14)-(5,31)", + "MessageFormat": "Message type '{0}' has no registered handler", + "Message": "Message type 'global::TestApp.GetSomethingQuery' has no registered handler", + "Category": "Mediator", + "CustomTags": [] + }, + { + "Id": "MO0005", + "Title": "Handler implements multiple mediator handler interfaces", + "Severity": "Error", + "WarningLevel": 0, + "Location": ": (7,13)-(7,25)", + "MessageFormat": "Handler '{0}' must implement exactly one mediator handler interface", + "Message": "Handler 'global::TestApp.MultiHandler' must implement exactly one mediator handler interface", + "Category": "Mediator", + "CustomTags": [] + } +] +``` diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md new file mode 100644 index 00000000000..4f61ee3eb4f --- /dev/null +++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md @@ -0,0 +1,33 @@ +# NoWarning_SingleHandlerInterface_NoDiagnostic + +```csharp +// + +#nullable enable +#pragma warning disable + +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")] + public static class TestsMediatorBuilderExtensions + { + public static global::Mocha.Mediator.IMediatorHostBuilder AddTests( + this global::Mocha.Mediator.IMediatorHostBuilder builder) + { + + // Register handler configurations + global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration(builder, + new global::Mocha.Mediator.MediatorHandlerConfiguration + { + HandlerType = typeof(global::TestApp.SomethingHappenedHandler), + MessageType = typeof(global::TestApp.SomethingHappened), + Kind = global::Mocha.Mediator.MediatorHandlerKind.Notification, + Delegate = global::Mocha.Mediator.PipelineBuilder.BuildNotificationPipeline() + }); + + return builder; + } + } +} + +``` diff --git a/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs index e86994af848..3af32349b2f 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/AddHandlerTests.cs @@ -4,14 +4,6 @@ namespace Mocha.Mediator.Tests; public class AddHandlerTests { - private static IServiceProvider BuildProvider(Action configure) - { - var services = new ServiceCollection(); - var builder = services.AddMediator(); - configure(builder); - return services.BuildServiceProvider(); - } - [Fact] public async Task AddHandler_Should_DispatchVoidCommand() { @@ -107,7 +99,7 @@ public void AddHandler_Should_Throw_When_TypeIsNotHandler() builder.AddHandler(); using var sp = services.BuildServiceProvider(); - // Act & Assert -- validation is deferred to Build() time, which happens + // Act & Assert - validation is deferred to Build() time, which happens // when MediatorRuntime is first resolved (triggered by IMediator resolution). Assert.Throws( () => sp.GetRequiredService()); @@ -126,7 +118,7 @@ public async Task AddHandler_Should_SupportFluentChaining() using var scope = sp.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); - // Act & Assert — all handler types work together + // Act & Assert - all handler types work together await mediator.SendAsync(new ManualVoidCommand("v")); Assert.True(ManualVoidCommandHandler.WasInvoked); @@ -140,6 +132,14 @@ public async Task AddHandler_Should_SupportFluentChaining() await mediator.PublishAsync(new ManualNotification("n")); Assert.True(ManualNotificationHandler1.WasInvoked); } + + private static IServiceProvider BuildProvider(Action configure) + { + var services = new ServiceCollection(); + var builder = services.AddMediator(); + configure(builder); + return services.BuildServiceProvider(); + } } public sealed record ManualVoidCommand(string Value) : ICommand; diff --git a/website/src/docs/mocha/v1/mediator/index.md b/website/src/docs/mocha/v1/mediator/index.md index 2184e3fbb95..cefde38d366 100644 --- a/website/src/docs/mocha/v1/mediator/index.md +++ b/website/src/docs/mocha/v1/mediator/index.md @@ -466,7 +466,7 @@ The source generator did not find a handler for your message type. Verify: If dispatch succeeds but your handler code does not execute, check that: -- Your middleware calls the `next` delegate -- a middleware that forgets to call `next` silently short-circuits the pipeline +- Your middleware calls the `next` delegate - a middleware that forgets to call `next` silently short-circuits the pipeline - You are not accidentally registering handlers manually in addition to the source-generated method, which could result in duplicate registrations ## The source-generated method does not appear @@ -474,7 +474,7 @@ If dispatch succeeds but your handler code does not execute, check that: If IntelliSense does not show `Add{ModuleName}()`: - Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj` -- Rebuild the project -- source generators run during compilation +- Rebuild the project - source generators run during compilation - Check the build output for analyzer warnings prefixed with `MO` ## `InvalidOperationException` when calling `AddHandler()` @@ -483,10 +483,10 @@ The type you passed does not implement any handler interface. Make sure `T` impl ## Named mediator returns wrong handlers -Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct `IMediatorHostBuilder` instance -- the one returned by the `AddMediator("name")` call for that name. +Each named mediator resolves handlers from the same DI container. Make sure you register each module's handlers on the correct `IMediatorHostBuilder` instance - the one returned by the `AddMediator("name")` call for that name. # Next steps You have a working mediator with CQRS dispatch. Here is where to go next: -- **Customize the pipeline:** [Pipeline & Middleware](/docs/mocha/v1/mediator/pipeline-and-middleware) -- add validation, logging, transactions, and other cross-cutting concerns. Configure notification publish modes and OpenTelemetry instrumentation. +- **Customize the pipeline:** [Pipeline & Middleware](/docs/mocha/v1/mediator/pipeline-and-middleware) - add validation, logging, transactions, and other cross-cutting concerns. Configure notification publish modes and OpenTelemetry instrumentation. From 5c8b9c76aff1f2f2f1b673d71fd6b22d6078df70 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Thu, 26 Mar 2026 21:32:53 +0000 Subject: [PATCH 4/4] cleanup --- .../src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs index 3b7c77db43d..59144942e17 100644 --- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs +++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs @@ -253,8 +253,8 @@ public MediatorRuntime Build(IServiceProvider applicationServices) var compiled = ImmutableArray.CreateBuilder(terminals.Count); for (var i = 0; i < terminals.Count; i++) { - compiled[i] = MediatorMiddlewareCompiler.Compile( - factoryCtx, terminals[i], middlewareConfigs, modifiers); + compiled.Add(MediatorMiddlewareCompiler.Compile( + factoryCtx, terminals[i], middlewareConfigs, modifiers)); } notificationPipelines[notificationType] = compiled.ToImmutable();