From 094f18feef3c40579f00eaa97244f08005c8c360 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 15:55:03 +0000 Subject: [PATCH 1/7] Adds Endpoint Configuration via Hanlders to Mocha --- .../IInMemoryMessagingTransportDescriptor.cs | 18 ++++++++ .../IInMemoryReceiveEndpointDescriptor.cs | 3 ++ .../InMemoryDispatchEndpointDescriptor.cs | 2 +- .../InMemoryMessagingTransportDescriptor.cs | 29 +++++++++++- .../InMemoryReceiveEndpointDescriptor.cs | 7 +++ .../Descriptors/InMemoryBindingDescriptor.cs | 2 +- .../Descriptors/InMemoryQueueDescriptor.cs | 2 +- .../Descriptors/InMemoryTopicDescriptor.cs | 2 +- .../IPostgresMessagingTransportDescriptor.cs | 8 ++++ .../IPostgresReceiveEndpointDescriptor.cs | 3 ++ .../PostgresMessagingTransportDescriptor.cs | 27 +++++++++++ .../PostgresReceiveEndpointDescriptor.cs | 8 ++++ .../IRabbitMQMessagingTransportDescriptor.cs | 8 ++++ .../IRabbitMQReceiveEndpointDescriptor.cs | 3 ++ .../RabbitMQMessagingTransportDescriptor.cs | 27 +++++++++++ .../RabbitMQReceiveEndpointDescriptor.cs | 7 +++ src/Mocha/src/Mocha/Assembly.cs | 1 + .../Descriptors/IReceiveEndpointDescriptor.cs | 7 +++ .../Descriptors/ReceiveEndpointDescriptor.cs | 11 +++++ .../Mocha/Transport/ConsumerConfigurator.cs | 25 ++++++++++ src/Mocha/src/Mocha/Transport/HandlerClaim.cs | 20 ++++++++ .../Mocha/Transport/HandlerConfigurator.cs | 25 ++++++++++ .../Mocha/Transport/IConsumerConfigurator.cs | 18 ++++++++ .../Mocha/Transport/IHandlerConfigurator.cs | 18 ++++++++ .../Transport/MessagingTransportDescriptor.cs | 46 +++++++++++++++++++ 25 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs create mode 100644 src/Mocha/src/Mocha/Transport/HandlerClaim.cs create mode 100644 src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs create mode 100644 src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs create mode 100644 src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs index 46a9cc3148c..c09cc732c31 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs @@ -74,4 +74,22 @@ public interface IInMemoryMessagingTransportDescriptor : IMessagingTransportDesc ReceiveMiddlewareConfiguration configuration, string? before = null, string? after = null); + + /// + /// Claims a handler for this in-memory transport and returns a configurator for its receive endpoint. + /// The handler will be bound to a convention-named endpoint on this transport during initialization. + /// + /// The handler type implementing . + /// A configurator that allows configuring the handler's receive endpoint. + new IHandlerConfigurator Handler() + where THandler : class, IHandler; + + /// + /// Claims a consumer for this in-memory transport and returns a configurator for its receive endpoint. + /// The consumer will be bound to a convention-named endpoint on this transport during initialization. + /// + /// The consumer type implementing . + /// A configurator that allows configuring the consumer's receive endpoint. + new IConsumerConfigurator Consumer() + where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs index 0029c7d9674..1e79eb319c6 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs @@ -9,6 +9,9 @@ public interface IInMemoryReceiveEndpointDescriptor : IReceiveEndpointDescriptor /// new IInMemoryReceiveEndpointDescriptor Handler() where THandler : class, IHandler; + /// + new IInMemoryReceiveEndpointDescriptor Handler(Type handlerType); + /// new IInMemoryReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer; diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryDispatchEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryDispatchEndpointDescriptor.cs index f90cf40d96d..e4308522912 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryDispatchEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryDispatchEndpointDescriptor.cs @@ -9,7 +9,7 @@ private InMemoryDispatchEndpointDescriptor(IMessagingConfigurationContext contex Configuration = new InMemoryDispatchEndpointConfiguration { Name = name, TopicName = name }; } - protected override InMemoryDispatchEndpointConfiguration Configuration { get; set; } + protected internal override InMemoryDispatchEndpointConfiguration Configuration { get; protected set; } public IInMemoryDispatchEndpointDescriptor ToQueue(string name) { diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs index 59e700ca46d..dfaeeaf4f10 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs @@ -28,7 +28,7 @@ public InMemoryMessagingTransportDescriptor(IMessagingSetupContext discoveryCont Configuration = new InMemoryTransportConfiguration(); } - protected override InMemoryTransportConfiguration Configuration { get; set; } + protected internal override InMemoryTransportConfiguration Configuration { get; protected set; } /// public new IInMemoryMessagingTransportDescriptor ModifyOptions(Action configure) @@ -108,6 +108,24 @@ public InMemoryMessagingTransportDescriptor(IMessagingSetupContext discoveryCont return this; } + /// + public new IHandlerConfigurator Handler() + where THandler : class, IHandler + { + var claim = new HandlerClaim { HandlerType = typeof(THandler) }; + HandlerClaims.Add(claim); + return new HandlerConfigurator(claim); + } + + /// + public new IConsumerConfigurator Consumer() + where TConsumer : class, IConsumer + { + var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; + HandlerClaims.Add(claim); + return new ConsumerConfigurator(claim); + } + /// public IInMemoryReceiveEndpointDescriptor Endpoint(string name) { @@ -184,6 +202,15 @@ public IInMemoryBindingDescriptor DeclareBinding(string exchange, string queue) /// The fully populated transport configuration ready for runtime initialization. public InMemoryTransportConfiguration CreateConfiguration() { + foreach (var claim in HandlerClaims) + { + var name = Context.Naming.GetReceiveEndpointName( + claim.HandlerType, ReceiveEndpointKind.Default); + var endpoint = (InMemoryReceiveEndpointDescriptor)Endpoint(name); + endpoint.Handler(claim.HandlerType); + claim.ConfigureEndpoint?.Invoke(endpoint); + } + Configuration.ReceiveEndpoints = _receiveEndpoints .Select(ReceiveEndpointConfiguration (e) => e.CreateConfiguration()) .ToList(); diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs index 7e96eee090d..ddbbb5dda53 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs @@ -17,6 +17,13 @@ internal InMemoryReceiveEndpointDescriptor(IMessagingConfigurationContext discov return this; } + public new IInMemoryReceiveEndpointDescriptor Handler(Type handlerType) + { + base.Handler(handlerType); + + return this; + } + public new IInMemoryReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer { base.Consumer(); diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryBindingDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryBindingDescriptor.cs index c66222eab0e..46b5d843824 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryBindingDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryBindingDescriptor.cs @@ -17,7 +17,7 @@ public InMemoryBindingDescriptor(IMessagingConfigurationContext context, string } /// - protected override InMemoryBindingConfiguration Configuration { get; set; } + protected internal override InMemoryBindingConfiguration Configuration { get; protected set; } /// public IInMemoryBindingDescriptor Source(string topicName) diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryQueueDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryQueueDescriptor.cs index 0aafc5d8a01..a9851eb414e 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryQueueDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryQueueDescriptor.cs @@ -18,7 +18,7 @@ public InMemoryQueueDescriptor(IMessagingConfigurationContext context, string na } /// - protected override InMemoryQueueConfiguration Configuration { get; set; } + protected internal override InMemoryQueueConfiguration Configuration { get; protected set; } /// public IInMemoryQueueDescriptor Name(string name) diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryTopicDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryTopicDescriptor.cs index 2b72e141a53..3ac4e22453b 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryTopicDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Topology/Descriptors/InMemoryTopicDescriptor.cs @@ -18,7 +18,7 @@ public InMemoryTopicDescriptor(IMessagingConfigurationContext context, string na } /// - protected override InMemoryTopicConfiguration Configuration { get; set; } + protected internal override InMemoryTopicConfiguration Configuration { get; protected set; } /// public IInMemoryTopicDescriptor Name(string name) diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs index 4da7bbf330f..1dcd4e2b66b 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs @@ -101,4 +101,12 @@ public interface IPostgresMessagingTransportDescriptor ReceiveMiddlewareConfiguration configuration, string? before = null, string? after = null); + + /// + new IHandlerConfigurator Handler() + where THandler : class, IHandler; + + /// + new IConsumerConfigurator Consumer() + where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs index 72e4a9285c9..235e3a23075 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs @@ -9,6 +9,9 @@ public interface IPostgresReceiveEndpointDescriptor : IReceiveEndpointDescriptor /// new IPostgresReceiveEndpointDescriptor Handler() where THandler : class, IHandler; + /// + new IPostgresReceiveEndpointDescriptor Handler(Type handlerType); + /// new IPostgresReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer; diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs index afe325388d8..d6d618a48b1 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs @@ -108,6 +108,24 @@ public PostgresMessagingTransportDescriptor(IMessagingSetupContext discoveryCont return this; } + /// + public new IHandlerConfigurator Handler() + where THandler : class, IHandler + { + var claim = new HandlerClaim { HandlerType = typeof(THandler) }; + HandlerClaims.Add(claim); + return new HandlerConfigurator(claim); + } + + /// + public new IConsumerConfigurator Consumer() + where TConsumer : class, IConsumer + { + var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; + HandlerClaims.Add(claim); + return new ConsumerConfigurator(claim); + } + /// public IPostgresMessagingTransportDescriptor AutoProvision(bool autoProvision = true) { @@ -210,6 +228,15 @@ public IPostgresSubscriptionDescriptor DeclareSubscription(string topic, string /// The fully populated transport configuration ready for runtime initialization. public PostgresTransportConfiguration CreateConfiguration() { + foreach (var claim in HandlerClaims) + { + var name = Context.Naming.GetReceiveEndpointName( + claim.HandlerType, ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Handler(claim.HandlerType); + claim.ConfigureEndpoint?.Invoke(endpoint); + } + Configuration.ReceiveEndpoints = _receiveEndpoints .Select(ReceiveEndpointConfiguration (e) => e.CreateConfiguration()) .ToList(); diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs index 8470ff48304..bd5354efd16 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs @@ -18,6 +18,14 @@ internal PostgresReceiveEndpointDescriptor(IMessagingConfigurationContext discov return this; } + /// + public new IPostgresReceiveEndpointDescriptor Handler(Type handlerType) + { + base.Handler(handlerType); + + return this; + } + /// public new IPostgresReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer { diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs index 565d540afdf..d9db57d74a1 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs @@ -101,4 +101,12 @@ IRabbitMQMessagingTransportDescriptor ConnectionProvider( ReceiveMiddlewareConfiguration configuration, string? before = null, string? after = null); + + /// + new IHandlerConfigurator Handler() + where THandler : class, IHandler; + + /// + new IConsumerConfigurator Consumer() + where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs index f5081a8e208..7b805141edc 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs @@ -9,6 +9,9 @@ public interface IRabbitMQReceiveEndpointDescriptor : IReceiveEndpointDescriptor /// new IRabbitMQReceiveEndpointDescriptor Handler() where THandler : class, IHandler; + /// + new IRabbitMQReceiveEndpointDescriptor Handler(Type handlerType); + /// new IRabbitMQReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer; diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs index 31531099116..e99fe933dcf 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs @@ -102,6 +102,24 @@ public RabbitMQMessagingTransportDescriptor(IMessagingSetupContext discoveryCont return this; } + /// + public new IHandlerConfigurator Handler() + where THandler : class, IHandler + { + var claim = new HandlerClaim { HandlerType = typeof(THandler) }; + HandlerClaims.Add(claim); + return new HandlerConfigurator(claim); + } + + /// + public new IConsumerConfigurator Consumer() + where TConsumer : class, IConsumer + { + var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; + HandlerClaims.Add(claim); + return new ConsumerConfigurator(claim); + } + /// public IRabbitMQMessagingTransportDescriptor AutoProvision(bool autoProvision = true) { @@ -202,6 +220,15 @@ public IRabbitMQBindingDescriptor DeclareBinding(string exchange, string queue) /// A fully populated ready for transport initialization. public RabbitMQTransportConfiguration CreateConfiguration() { + foreach (var claim in HandlerClaims) + { + var name = Context.Naming.GetReceiveEndpointName( + claim.HandlerType, ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Handler(claim.HandlerType); + claim.ConfigureEndpoint?.Invoke(endpoint); + } + Configuration.ReceiveEndpoints = _receiveEndpoints .Select(ReceiveEndpointConfiguration (e) => e.CreateConfiguration()) .ToList(); diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs index b6fb9fd5555..9c94b465e63 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs @@ -21,6 +21,13 @@ private RabbitMQReceiveEndpointDescriptor(IMessagingConfigurationContext discove return this; } + public new IRabbitMQReceiveEndpointDescriptor Handler(Type handlerType) + { + base.Handler(handlerType); + + return this; + } + public new IRabbitMQReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer { base.Consumer(); diff --git a/src/Mocha/src/Mocha/Assembly.cs b/src/Mocha/src/Mocha/Assembly.cs index 2c4eb2e08f2..05ed5eeda12 100644 --- a/src/Mocha/src/Mocha/Assembly.cs +++ b/src/Mocha/src/Mocha/Assembly.cs @@ -10,6 +10,7 @@ [assembly: InternalsVisibleTo("Mocha.EntityFrameworkCore")] [assembly: InternalsVisibleTo("Mocha.Transport.RabbitMQ")] [assembly: InternalsVisibleTo("Mocha.Transport.Postgres")] +[assembly: InternalsVisibleTo("Mocha.Transport.InMemory")] [assembly: InternalsVisibleTo("Mocha.Tests")] [assembly: InternalsVisibleTo("Mocha.Sagas.TestHelpers")] [assembly: InternalsVisibleTo("Mocha.Sagas.Tests")] diff --git a/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs index ea9b3d26e0c..4b12c58eac4 100644 --- a/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs @@ -15,6 +15,13 @@ public interface IReceiveEndpointDescriptor /// The descriptor instance for method chaining. IReceiveEndpointDescriptor Handler() where THandler : class, IHandler; + /// + /// Binds a handler to this receive endpoint by its runtime type. + /// + /// The handler type to bind. + /// The descriptor instance for method chaining. + IReceiveEndpointDescriptor Handler(Type handlerType); + /// /// Binds a consumer to this receive endpoint, ensuring its messages are consumed on this /// endpoint. diff --git a/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs index 16fb62c78d7..9b641ee3412 100644 --- a/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs @@ -22,6 +22,17 @@ public IReceiveEndpointDescriptor Consumer() where TConsumer : cla return this; } + /// + /// Binds a handler to this receive endpoint by its runtime type. + /// + /// The handler type to bind. + /// The descriptor instance for method chaining. + public IReceiveEndpointDescriptor Handler(Type handlerType) + { + Configuration.ConsumerIdentities.Add(handlerType); + return this; + } + public IReceiveEndpointDescriptor Kind(ReceiveEndpointKind kind) { Configuration.Kind = kind; diff --git a/src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs new file mode 100644 index 00000000000..45babca31ff --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs @@ -0,0 +1,25 @@ +namespace Mocha; + +/// +/// Internal implementation of that +/// captures endpoint configuration actions on the underlying . +/// +/// The endpoint descriptor type exposed by the transport. +internal sealed class ConsumerConfigurator + : IConsumerConfigurator +{ + private readonly HandlerClaim _claim; + + internal ConsumerConfigurator(HandlerClaim claim) => _claim = claim; + + /// + public IConsumerConfigurator ConfigureEndpoint( + Action configure) + { + var prev = _claim.ConfigureEndpoint; + _claim.ConfigureEndpoint = prev is null + ? obj => configure((TEndpointDescriptor)obj) + : obj => { prev(obj); configure((TEndpointDescriptor)obj); }; + return this; + } +} diff --git a/src/Mocha/src/Mocha/Transport/HandlerClaim.cs b/src/Mocha/src/Mocha/Transport/HandlerClaim.cs new file mode 100644 index 00000000000..9236cf9052d --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/HandlerClaim.cs @@ -0,0 +1,20 @@ +namespace Mocha; + +/// +/// Stores a transport-level handler claim, capturing the handler type and an optional +/// endpoint configuration action to apply when the claim is materialized. +/// +internal sealed class HandlerClaim +{ + /// + /// Gets the handler type that is claimed by the transport. + /// + public required Type HandlerType { get; init; } + + /// + /// Gets or sets an optional configuration action applied to the receive endpoint descriptor + /// when the claim is materialized. The delegate accepts the endpoint descriptor as + /// and casts internally to the transport-specific type. + /// + public Action? ConfigureEndpoint { get; set; } +} diff --git a/src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs new file mode 100644 index 00000000000..16faf84bdfa --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs @@ -0,0 +1,25 @@ +namespace Mocha; + +/// +/// Internal implementation of that +/// captures endpoint configuration actions on the underlying . +/// +/// The endpoint descriptor type exposed by the transport. +internal sealed class HandlerConfigurator + : IHandlerConfigurator +{ + private readonly HandlerClaim _claim; + + internal HandlerConfigurator(HandlerClaim claim) => _claim = claim; + + /// + public IHandlerConfigurator ConfigureEndpoint( + Action configure) + { + var prev = _claim.ConfigureEndpoint; + _claim.ConfigureEndpoint = prev is null + ? obj => configure((TEndpointDescriptor)obj) + : obj => { prev(obj); configure((TEndpointDescriptor)obj); }; + return this; + } +} diff --git a/src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs new file mode 100644 index 00000000000..371210964be --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs @@ -0,0 +1,18 @@ +namespace Mocha; + +/// +/// Fluent configurator returned by a transport's Consumer<T>() method. +/// Allows configuring the receive endpoint for a claimed consumer. +/// +/// The endpoint descriptor type exposed by the transport. +public interface IConsumerConfigurator +{ + /// + /// Configures the receive endpoint for this consumer. + /// Can be called multiple times — actions compose in order. + /// + /// A delegate to configure the endpoint descriptor. + /// This configurator for method chaining. + IConsumerConfigurator ConfigureEndpoint( + Action configure); +} diff --git a/src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs new file mode 100644 index 00000000000..91b0433ee97 --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs @@ -0,0 +1,18 @@ +namespace Mocha; + +/// +/// Fluent configurator returned by a transport's Handler<T>() method. +/// Allows configuring the receive endpoint for a claimed handler. +/// +/// The endpoint descriptor type exposed by the transport. +public interface IHandlerConfigurator +{ + /// + /// Configures the receive endpoint for this handler. + /// Can be called multiple times — actions compose in order. + /// + /// A delegate to configure the endpoint descriptor. + /// This configurator for method chaining. + IHandlerConfigurator ConfigureEndpoint( + Action configure); +} diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs index 3243dbbf7e7..f3f67f7729a 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs @@ -90,6 +90,24 @@ IMessagingTransportDescriptor UseReceive( ReceiveMiddlewareConfiguration configuration, string? before = null, string? after = null); + + /// + /// Claims a handler for this transport and returns a configurator for its receive endpoint. + /// The handler will be bound to a convention-named endpoint on this transport during initialization. + /// + /// The handler type implementing . + /// A configurator that allows configuring the handler's receive endpoint. + IHandlerConfigurator> Handler() + where THandler : class, IHandler; + + /// + /// Claims a consumer for this transport and returns a configurator for its receive endpoint. + /// The consumer will be bound to a convention-named endpoint on this transport during initialization. + /// + /// The consumer type implementing . + /// A configurator that allows configuring the consumer's receive endpoint. + IConsumerConfigurator> Consumer() + where TConsumer : class, IConsumer; } /// @@ -102,6 +120,14 @@ public abstract class MessagingTransportDescriptor(IMessagingSetupContext con : MessagingDescriptorBase(context) , IMessagingTransportDescriptor where T : MessagingTransportConfiguration { + private readonly List _handlerClaims = []; + + /// + /// Gets the handler claims registered on this transport descriptor, for use by + /// transport-specific subclasses during configuration materialization. + /// + private protected List HandlerClaims => _handlerClaims; + /// public IMessagingTransportDescriptor ModifyOptions(Action configure) { @@ -209,6 +235,26 @@ public IMessagingTransportDescriptor UseReceive( return this; } + /// + public IHandlerConfigurator> + Handler() where THandler : class, IHandler + { + var claim = new HandlerClaim { HandlerType = typeof(THandler) }; + _handlerClaims.Add(claim); + return new HandlerConfigurator< + IReceiveEndpointDescriptor>(claim); + } + + /// + public IConsumerConfigurator> + Consumer() where TConsumer : class, IConsumer + { + var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; + _handlerClaims.Add(claim); + return new ConsumerConfigurator< + IReceiveEndpointDescriptor>(claim); + } + /// /// Returns this descriptor as an extension point for the transport configuration, allowing additional /// configuration to be layered by external modules. From 1bd81cdd4b44bbd6aa73ed7b120b9cef54bec7c0 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 17:30:42 +0000 Subject: [PATCH 2/7] cleanup --- .../IInMemoryMessagingTransportDescriptor.cs | 4 +- .../IInMemoryReceiveEndpointDescriptor.cs | 3 + .../InMemoryMessagingTransportDescriptor.cs | 27 +++---- .../InMemoryReceiveEndpointDescriptor.cs | 7 ++ .../IPostgresMessagingTransportDescriptor.cs | 4 +- .../IPostgresReceiveEndpointDescriptor.cs | 3 + .../PostgresMessagingTransportDescriptor.cs | 27 +++---- .../PostgresReceiveEndpointDescriptor.cs | 8 ++ .../IRabbitMQMessagingTransportDescriptor.cs | 4 +- .../IRabbitMQReceiveEndpointDescriptor.cs | 3 + .../RabbitMQMessagingTransportDescriptor.cs | 27 +++---- .../RabbitMQReceiveEndpointDescriptor.cs | 7 ++ .../Descriptors/IReceiveEndpointDescriptor.cs | 7 ++ .../Descriptors/ReceiveEndpointDescriptor.cs | 11 +++ .../Mocha/Transport/ConsumerConfigurator.cs | 25 ------ src/Mocha/src/Mocha/Transport/HandlerClaim.cs | 20 ----- .../Mocha/Transport/HandlerConfigurator.cs | 25 ------ ...r.cs => ITransportConsumerConfigurator.cs} | 7 +- ...or.cs => ITransportHandlerConfigurator.cs} | 7 +- .../Transport/MessagingTransportDescriptor.cs | 30 ++----- .../TransportConsumerConfigurator.cs | 18 +++++ .../Transport/TransportHandlerConfigurator.cs | 18 +++++ .../docs/mocha/v1/routing-and-endpoints.md | 81 ++++++++++++++++++- 23 files changed, 212 insertions(+), 161 deletions(-) delete mode 100644 src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs delete mode 100644 src/Mocha/src/Mocha/Transport/HandlerClaim.cs delete mode 100644 src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs rename src/Mocha/src/Mocha/Transport/{IConsumerConfigurator.cs => ITransportConsumerConfigurator.cs} (69%) rename src/Mocha/src/Mocha/Transport/{IHandlerConfigurator.cs => ITransportHandlerConfigurator.cs} (69%) create mode 100644 src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs create mode 100644 src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs index c09cc732c31..2cb64420e9f 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs @@ -81,7 +81,7 @@ public interface IInMemoryMessagingTransportDescriptor : IMessagingTransportDesc /// /// The handler type implementing . /// A configurator that allows configuring the handler's receive endpoint. - new IHandlerConfigurator Handler() + new ITransportHandlerConfigurator Handler() where THandler : class, IHandler; /// @@ -90,6 +90,6 @@ public interface IInMemoryMessagingTransportDescriptor : IMessagingTransportDesc /// /// The consumer type implementing . /// A configurator that allows configuring the consumer's receive endpoint. - new IConsumerConfigurator Consumer() + new ITransportConsumerConfigurator Consumer() where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs index 1e79eb319c6..feb5b970305 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryReceiveEndpointDescriptor.cs @@ -12,6 +12,9 @@ public interface IInMemoryReceiveEndpointDescriptor : IReceiveEndpointDescriptor /// new IInMemoryReceiveEndpointDescriptor Handler(Type handlerType); + /// + new IInMemoryReceiveEndpointDescriptor Consumer(Type consumerType); + /// new IInMemoryReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer; diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs index dfaeeaf4f10..2a8a15c2797 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs @@ -109,21 +109,23 @@ public InMemoryMessagingTransportDescriptor(IMessagingSetupContext discoveryCont } /// - public new IHandlerConfigurator Handler() + public new ITransportHandlerConfigurator Handler() where THandler : class, IHandler { - var claim = new HandlerClaim { HandlerType = typeof(THandler) }; - HandlerClaims.Add(claim); - return new HandlerConfigurator(claim); + var name = Context.Naming.GetReceiveEndpointName(typeof(THandler), ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Handler(typeof(THandler)); + return new TransportHandlerConfigurator(endpoint); } /// - public new IConsumerConfigurator Consumer() + public new ITransportConsumerConfigurator Consumer() where TConsumer : class, IConsumer { - var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; - HandlerClaims.Add(claim); - return new ConsumerConfigurator(claim); + var name = Context.Naming.GetReceiveEndpointName(typeof(TConsumer), ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Consumer(typeof(TConsumer)); + return new TransportConsumerConfigurator(endpoint); } /// @@ -202,15 +204,6 @@ public IInMemoryBindingDescriptor DeclareBinding(string exchange, string queue) /// The fully populated transport configuration ready for runtime initialization. public InMemoryTransportConfiguration CreateConfiguration() { - foreach (var claim in HandlerClaims) - { - var name = Context.Naming.GetReceiveEndpointName( - claim.HandlerType, ReceiveEndpointKind.Default); - var endpoint = (InMemoryReceiveEndpointDescriptor)Endpoint(name); - endpoint.Handler(claim.HandlerType); - claim.ConfigureEndpoint?.Invoke(endpoint); - } - Configuration.ReceiveEndpoints = _receiveEndpoints .Select(ReceiveEndpointConfiguration (e) => e.CreateConfiguration()) .ToList(); diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs index ddbbb5dda53..0262dde3d6d 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryReceiveEndpointDescriptor.cs @@ -24,6 +24,13 @@ internal InMemoryReceiveEndpointDescriptor(IMessagingConfigurationContext discov return this; } + public new IInMemoryReceiveEndpointDescriptor Consumer(Type consumerType) + { + base.Consumer(consumerType); + + return this; + } + public new IInMemoryReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer { base.Consumer(); diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs index 1dcd4e2b66b..f4e477b34d5 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs @@ -103,10 +103,10 @@ public interface IPostgresMessagingTransportDescriptor string? after = null); /// - new IHandlerConfigurator Handler() + new ITransportHandlerConfigurator Handler() where THandler : class, IHandler; /// - new IConsumerConfigurator Consumer() + new ITransportConsumerConfigurator Consumer() where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs index 235e3a23075..5cbaced2cd0 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresReceiveEndpointDescriptor.cs @@ -12,6 +12,9 @@ public interface IPostgresReceiveEndpointDescriptor : IReceiveEndpointDescriptor /// new IPostgresReceiveEndpointDescriptor Handler(Type handlerType); + /// + new IPostgresReceiveEndpointDescriptor Consumer(Type consumerType); + /// new IPostgresReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer; diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs index d6d618a48b1..8ebdbd79921 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs @@ -109,21 +109,23 @@ public PostgresMessagingTransportDescriptor(IMessagingSetupContext discoveryCont } /// - public new IHandlerConfigurator Handler() + public new ITransportHandlerConfigurator Handler() where THandler : class, IHandler { - var claim = new HandlerClaim { HandlerType = typeof(THandler) }; - HandlerClaims.Add(claim); - return new HandlerConfigurator(claim); + var name = Context.Naming.GetReceiveEndpointName(typeof(THandler), ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Handler(typeof(THandler)); + return new TransportHandlerConfigurator(endpoint); } /// - public new IConsumerConfigurator Consumer() + public new ITransportConsumerConfigurator Consumer() where TConsumer : class, IConsumer { - var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; - HandlerClaims.Add(claim); - return new ConsumerConfigurator(claim); + var name = Context.Naming.GetReceiveEndpointName(typeof(TConsumer), ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Consumer(typeof(TConsumer)); + return new TransportConsumerConfigurator(endpoint); } /// @@ -228,15 +230,6 @@ public IPostgresSubscriptionDescriptor DeclareSubscription(string topic, string /// The fully populated transport configuration ready for runtime initialization. public PostgresTransportConfiguration CreateConfiguration() { - foreach (var claim in HandlerClaims) - { - var name = Context.Naming.GetReceiveEndpointName( - claim.HandlerType, ReceiveEndpointKind.Default); - var endpoint = Endpoint(name); - endpoint.Handler(claim.HandlerType); - claim.ConfigureEndpoint?.Invoke(endpoint); - } - Configuration.ReceiveEndpoints = _receiveEndpoints .Select(ReceiveEndpointConfiguration (e) => e.CreateConfiguration()) .ToList(); diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs index bd5354efd16..0b620fb59b9 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresReceiveEndpointDescriptor.cs @@ -26,6 +26,14 @@ internal PostgresReceiveEndpointDescriptor(IMessagingConfigurationContext discov return this; } + /// + public new IPostgresReceiveEndpointDescriptor Consumer(Type consumerType) + { + base.Consumer(consumerType); + + return this; + } + /// public new IPostgresReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer { diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs index d9db57d74a1..637463dd430 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs @@ -103,10 +103,10 @@ IRabbitMQMessagingTransportDescriptor ConnectionProvider( string? after = null); /// - new IHandlerConfigurator Handler() + new ITransportHandlerConfigurator Handler() where THandler : class, IHandler; /// - new IConsumerConfigurator Consumer() + new ITransportConsumerConfigurator Consumer() where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs index 7b805141edc..815a29675db 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQReceiveEndpointDescriptor.cs @@ -12,6 +12,9 @@ public interface IRabbitMQReceiveEndpointDescriptor : IReceiveEndpointDescriptor /// new IRabbitMQReceiveEndpointDescriptor Handler(Type handlerType); + /// + new IRabbitMQReceiveEndpointDescriptor Consumer(Type consumerType); + /// new IRabbitMQReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer; diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs index e99fe933dcf..a8f33de54b3 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs @@ -103,21 +103,23 @@ public RabbitMQMessagingTransportDescriptor(IMessagingSetupContext discoveryCont } /// - public new IHandlerConfigurator Handler() + public new ITransportHandlerConfigurator Handler() where THandler : class, IHandler { - var claim = new HandlerClaim { HandlerType = typeof(THandler) }; - HandlerClaims.Add(claim); - return new HandlerConfigurator(claim); + var name = Context.Naming.GetReceiveEndpointName(typeof(THandler), ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Handler(typeof(THandler)); + return new TransportHandlerConfigurator(endpoint); } /// - public new IConsumerConfigurator Consumer() + public new ITransportConsumerConfigurator Consumer() where TConsumer : class, IConsumer { - var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; - HandlerClaims.Add(claim); - return new ConsumerConfigurator(claim); + var name = Context.Naming.GetReceiveEndpointName(typeof(TConsumer), ReceiveEndpointKind.Default); + var endpoint = Endpoint(name); + endpoint.Consumer(typeof(TConsumer)); + return new TransportConsumerConfigurator(endpoint); } /// @@ -220,15 +222,6 @@ public IRabbitMQBindingDescriptor DeclareBinding(string exchange, string queue) /// A fully populated ready for transport initialization. public RabbitMQTransportConfiguration CreateConfiguration() { - foreach (var claim in HandlerClaims) - { - var name = Context.Naming.GetReceiveEndpointName( - claim.HandlerType, ReceiveEndpointKind.Default); - var endpoint = Endpoint(name); - endpoint.Handler(claim.HandlerType); - claim.ConfigureEndpoint?.Invoke(endpoint); - } - Configuration.ReceiveEndpoints = _receiveEndpoints .Select(ReceiveEndpointConfiguration (e) => e.CreateConfiguration()) .ToList(); diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs index 9c94b465e63..da315f36159 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQReceiveEndpointDescriptor.cs @@ -28,6 +28,13 @@ private RabbitMQReceiveEndpointDescriptor(IMessagingConfigurationContext discove return this; } + public new IRabbitMQReceiveEndpointDescriptor Consumer(Type consumerType) + { + base.Consumer(consumerType); + + return this; + } + public new IRabbitMQReceiveEndpointDescriptor Consumer() where TConsumer : class, IConsumer { base.Consumer(); diff --git a/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs index 4b12c58eac4..4cffa39706f 100644 --- a/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha/Endpoints/Descriptors/IReceiveEndpointDescriptor.cs @@ -22,6 +22,13 @@ public interface IReceiveEndpointDescriptor /// The descriptor instance for method chaining. IReceiveEndpointDescriptor Handler(Type handlerType); + /// + /// Binds a consumer to this receive endpoint by its runtime type. + /// + /// The consumer type to bind. + /// The descriptor instance for method chaining. + IReceiveEndpointDescriptor Consumer(Type consumerType); + /// /// Binds a consumer to this receive endpoint, ensuring its messages are consumed on this /// endpoint. diff --git a/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs b/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs index 9b641ee3412..b3b95f36aa0 100644 --- a/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs +++ b/src/Mocha/src/Mocha/Endpoints/Descriptors/ReceiveEndpointDescriptor.cs @@ -33,6 +33,17 @@ public IReceiveEndpointDescriptor Handler(Type handlerType) return this; } + /// + /// Binds a consumer to this receive endpoint by its runtime type. + /// + /// The consumer type to bind. + /// The descriptor instance for method chaining. + public IReceiveEndpointDescriptor Consumer(Type consumerType) + { + Configuration.ConsumerIdentities.Add(consumerType); + return this; + } + public IReceiveEndpointDescriptor Kind(ReceiveEndpointKind kind) { Configuration.Kind = kind; diff --git a/src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs deleted file mode 100644 index 45babca31ff..00000000000 --- a/src/Mocha/src/Mocha/Transport/ConsumerConfigurator.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Mocha; - -/// -/// Internal implementation of that -/// captures endpoint configuration actions on the underlying . -/// -/// The endpoint descriptor type exposed by the transport. -internal sealed class ConsumerConfigurator - : IConsumerConfigurator -{ - private readonly HandlerClaim _claim; - - internal ConsumerConfigurator(HandlerClaim claim) => _claim = claim; - - /// - public IConsumerConfigurator ConfigureEndpoint( - Action configure) - { - var prev = _claim.ConfigureEndpoint; - _claim.ConfigureEndpoint = prev is null - ? obj => configure((TEndpointDescriptor)obj) - : obj => { prev(obj); configure((TEndpointDescriptor)obj); }; - return this; - } -} diff --git a/src/Mocha/src/Mocha/Transport/HandlerClaim.cs b/src/Mocha/src/Mocha/Transport/HandlerClaim.cs deleted file mode 100644 index 9236cf9052d..00000000000 --- a/src/Mocha/src/Mocha/Transport/HandlerClaim.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Mocha; - -/// -/// Stores a transport-level handler claim, capturing the handler type and an optional -/// endpoint configuration action to apply when the claim is materialized. -/// -internal sealed class HandlerClaim -{ - /// - /// Gets the handler type that is claimed by the transport. - /// - public required Type HandlerType { get; init; } - - /// - /// Gets or sets an optional configuration action applied to the receive endpoint descriptor - /// when the claim is materialized. The delegate accepts the endpoint descriptor as - /// and casts internally to the transport-specific type. - /// - public Action? ConfigureEndpoint { get; set; } -} diff --git a/src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs deleted file mode 100644 index 16faf84bdfa..00000000000 --- a/src/Mocha/src/Mocha/Transport/HandlerConfigurator.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Mocha; - -/// -/// Internal implementation of that -/// captures endpoint configuration actions on the underlying . -/// -/// The endpoint descriptor type exposed by the transport. -internal sealed class HandlerConfigurator - : IHandlerConfigurator -{ - private readonly HandlerClaim _claim; - - internal HandlerConfigurator(HandlerClaim claim) => _claim = claim; - - /// - public IHandlerConfigurator ConfigureEndpoint( - Action configure) - { - var prev = _claim.ConfigureEndpoint; - _claim.ConfigureEndpoint = prev is null - ? obj => configure((TEndpointDescriptor)obj) - : obj => { prev(obj); configure((TEndpointDescriptor)obj); }; - return this; - } -} diff --git a/src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/ITransportConsumerConfigurator.cs similarity index 69% rename from src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs rename to src/Mocha/src/Mocha/Transport/ITransportConsumerConfigurator.cs index 371210964be..486afafc795 100644 --- a/src/Mocha/src/Mocha/Transport/IConsumerConfigurator.cs +++ b/src/Mocha/src/Mocha/Transport/ITransportConsumerConfigurator.cs @@ -5,14 +5,13 @@ namespace Mocha; /// Allows configuring the receive endpoint for a claimed consumer. /// /// The endpoint descriptor type exposed by the transport. -public interface IConsumerConfigurator +public interface ITransportConsumerConfigurator { /// /// Configures the receive endpoint for this consumer. - /// Can be called multiple times — actions compose in order. + /// Can be called multiple times - actions compose in order. /// /// A delegate to configure the endpoint descriptor. /// This configurator for method chaining. - IConsumerConfigurator ConfigureEndpoint( - Action configure); + ITransportConsumerConfigurator ConfigureEndpoint(Action configure); } diff --git a/src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/ITransportHandlerConfigurator.cs similarity index 69% rename from src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs rename to src/Mocha/src/Mocha/Transport/ITransportHandlerConfigurator.cs index 91b0433ee97..b01fe713aaf 100644 --- a/src/Mocha/src/Mocha/Transport/IHandlerConfigurator.cs +++ b/src/Mocha/src/Mocha/Transport/ITransportHandlerConfigurator.cs @@ -5,14 +5,13 @@ namespace Mocha; /// Allows configuring the receive endpoint for a claimed handler. /// /// The endpoint descriptor type exposed by the transport. -public interface IHandlerConfigurator +public interface ITransportHandlerConfigurator { /// /// Configures the receive endpoint for this handler. - /// Can be called multiple times — actions compose in order. + /// Can be called multiple times - actions compose in order. /// /// A delegate to configure the endpoint descriptor. /// This configurator for method chaining. - IHandlerConfigurator ConfigureEndpoint( - Action configure); + ITransportHandlerConfigurator ConfigureEndpoint(Action configure); } diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs index f3f67f7729a..c6365bcbf9b 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs @@ -97,7 +97,7 @@ IMessagingTransportDescriptor UseReceive( /// /// The handler type implementing . /// A configurator that allows configuring the handler's receive endpoint. - IHandlerConfigurator> Handler() + ITransportHandlerConfigurator> Handler() where THandler : class, IHandler; /// @@ -106,7 +106,7 @@ IHandlerConfigurator> H /// /// The consumer type implementing . /// A configurator that allows configuring the consumer's receive endpoint. - IConsumerConfigurator> Consumer() + ITransportConsumerConfigurator> Consumer() where TConsumer : class, IConsumer; } @@ -120,14 +120,6 @@ public abstract class MessagingTransportDescriptor(IMessagingSetupContext con : MessagingDescriptorBase(context) , IMessagingTransportDescriptor where T : MessagingTransportConfiguration { - private readonly List _handlerClaims = []; - - /// - /// Gets the handler claims registered on this transport descriptor, for use by - /// transport-specific subclasses during configuration materialization. - /// - private protected List HandlerClaims => _handlerClaims; - /// public IMessagingTransportDescriptor ModifyOptions(Action configure) { @@ -236,24 +228,14 @@ public IMessagingTransportDescriptor UseReceive( } /// - public IHandlerConfigurator> + public virtual ITransportHandlerConfigurator> Handler() where THandler : class, IHandler - { - var claim = new HandlerClaim { HandlerType = typeof(THandler) }; - _handlerClaims.Add(claim); - return new HandlerConfigurator< - IReceiveEndpointDescriptor>(claim); - } + => throw new NotSupportedException("Use the transport-specific Handler() method."); /// - public IConsumerConfigurator> + public virtual ITransportConsumerConfigurator> Consumer() where TConsumer : class, IConsumer - { - var claim = new HandlerClaim { HandlerType = typeof(TConsumer) }; - _handlerClaims.Add(claim); - return new ConsumerConfigurator< - IReceiveEndpointDescriptor>(claim); - } + => throw new NotSupportedException("Use the transport-specific Consumer() method."); /// /// Returns this descriptor as an extension point for the transport configuration, allowing additional diff --git a/src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs new file mode 100644 index 00000000000..11ed6000a13 --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs @@ -0,0 +1,18 @@ +namespace Mocha; + +/// +/// Internal implementation of that +/// passes endpoint configuration actions directly to the underlying endpoint descriptor. +/// +/// The endpoint descriptor type exposed by the transport. +internal sealed class TransportConsumerConfigurator(TEndpointDescriptor endpoint) + : ITransportConsumerConfigurator +{ + /// + public ITransportConsumerConfigurator ConfigureEndpoint( + Action configure) + { + configure(endpoint); + return this; + } +} diff --git a/src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs new file mode 100644 index 00000000000..5e7c433ae0b --- /dev/null +++ b/src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs @@ -0,0 +1,18 @@ +namespace Mocha; + +/// +/// Internal implementation of that +/// passes endpoint configuration actions directly to the underlying endpoint descriptor. +/// +/// The endpoint descriptor type exposed by the transport. +internal sealed class TransportHandlerConfigurator(TEndpointDescriptor endpoint) + : ITransportHandlerConfigurator +{ + /// + public ITransportHandlerConfigurator ConfigureEndpoint( + Action configure) + { + configure(endpoint); + return this; + } +} diff --git a/website/src/docs/mocha/v1/routing-and-endpoints.md b/website/src/docs/mocha/v1/routing-and-endpoints.md index 0fd04412811..8cf1429d94d 100644 --- a/website/src/docs/mocha/v1/routing-and-endpoints.md +++ b/website/src/docs/mocha/v1/routing-and-endpoints.md @@ -227,6 +227,63 @@ builder.Services .AddRabbitMQ(); ``` +## Configure handler endpoints + +When you want convention-based endpoint naming but need to customize specific handler endpoints, use `transport.Handler()`. This claims the handler for the transport and gives you access to the endpoint descriptor through `ConfigureEndpoint()`: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddEventHandler() + .AddRabbitMQ(transport => + { + // Claim OrderPlacedHandler and configure its convention-named endpoint + transport.Handler() + .ConfigureEndpoint(ep => ep.MaxConcurrency(10)) + .ConfigureEndpoint(ep => ep.FaultEndpoint("order-errors")); + }); +``` + +`PaymentReceivedHandler` still gets an auto-discovered endpoint with default settings. `OrderPlacedHandler` gets the same convention-derived endpoint name, but with concurrency capped at 10 and a custom fault endpoint. + +Multiple `ConfigureEndpoint()` calls on the same handler compose in declaration order. + +The same pattern works for consumers: + +```csharp +transport.Consumer() + .ConfigureEndpoint(ep => ep.MaxConcurrency(3)); +``` + +Inside `ConfigureEndpoint()`, you have access to transport-specific settings. To set prefetch on a RabbitMQ endpoint: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddRabbitMQ(transport => + { + transport.Handler() + .ConfigureEndpoint(ep => ep.MaxPrefetch(50)); + }); +``` + +For PostgreSQL, configure the batch size: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddPostgres(transport => + { + transport.Handler() + .ConfigureEndpoint(ep => ep.MaxBatchSize(100)); + }); +``` + +This approach sits between implicit and explicit binding: you keep automatic endpoint naming but gain per-handler configuration. See the [RabbitMQ](/docs/mocha/v1/transports/rabbitmq), [PostgreSQL](/docs/mocha/v1/transports/postgres), and [InMemory](/docs/mocha/v1/transports/in-memory) transport pages for the full set of transport-specific endpoint settings. + ## Explicit binding Switch to explicit binding when you need full control over which handlers run on which endpoints. With explicit binding, the transport does not auto-discover endpoints - you must declare each one: @@ -249,9 +306,11 @@ builder.Services Both handlers now consume from the same `combined-orders` queue. Without explicit binding, they would each get their own endpoint. -## Configure endpoint settings +## Named endpoints vs. handler claims -Whether you use implicit or explicit binding, you can configure per-endpoint settings through the endpoint descriptor: +You can configure per-endpoint settings in two ways: through a named endpoint, or through `transport.Handler()` which derives the endpoint name from conventions. + +To configure a named endpoint directly: ```csharp builder.Services @@ -267,6 +326,24 @@ builder.Services }); ``` +To configure through a handler claim, which derives the endpoint name from conventions: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddRabbitMQ(rabbit => + { + rabbit.Handler() + .ConfigureEndpoint(ep => ep + .MaxConcurrency(5) + .FaultEndpoint("order-errors") + .SkippedEndpoint("order-skipped")); + }); +``` + +Use `transport.Endpoint("name")` when you need to control the endpoint name or bind multiple handlers to the same endpoint. Use `transport.Handler()` when you want the convention-derived name and are configuring a single handler's endpoint. + For outbound endpoints: ```csharp From d473665c63e602e68e9f8d09c5b1dc15b5c90c17 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 17:43:46 +0000 Subject: [PATCH 3/7] cleanup --- .../IInMemoryMessagingTransportDescriptor.cs | 4 +-- .../InMemoryMessagingTransportDescriptor.cs | 8 +++--- .../IPostgresMessagingTransportDescriptor.cs | 8 +++--- .../PostgresMessagingTransportDescriptor.cs | 8 +++--- .../IRabbitMQMessagingTransportDescriptor.cs | 8 +++--- .../RabbitMQMessagingTransportDescriptor.cs | 8 +++--- ... IMessagingTransportConsumerDescriptor.cs} | 4 +-- ...> IMessagingTransportHandlerDescriptor.cs} | 4 +-- ...> MessagingTransportConsumerDescriptor.cs} | 8 +++--- .../Transport/MessagingTransportDescriptor.cs | 28 ------------------- ...=> MessagingTransportHandlerDescriptor.cs} | 8 +++--- 11 files changed, 34 insertions(+), 62 deletions(-) rename src/Mocha/src/Mocha/Transport/{ITransportConsumerConfigurator.cs => IMessagingTransportConsumerDescriptor.cs} (76%) rename src/Mocha/src/Mocha/Transport/{ITransportHandlerConfigurator.cs => IMessagingTransportHandlerDescriptor.cs} (76%) rename src/Mocha/src/Mocha/Transport/{TransportHandlerConfigurator.cs => MessagingTransportConsumerDescriptor.cs} (50%) rename src/Mocha/src/Mocha/Transport/{TransportConsumerConfigurator.cs => MessagingTransportHandlerDescriptor.cs} (50%) diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs index 2cb64420e9f..9e2fb2aaca4 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/IInMemoryMessagingTransportDescriptor.cs @@ -81,7 +81,7 @@ public interface IInMemoryMessagingTransportDescriptor : IMessagingTransportDesc /// /// The handler type implementing . /// A configurator that allows configuring the handler's receive endpoint. - new ITransportHandlerConfigurator Handler() + IMessagingTransportHandlerDescriptor Handler() where THandler : class, IHandler; /// @@ -90,6 +90,6 @@ public interface IInMemoryMessagingTransportDescriptor : IMessagingTransportDesc /// /// The consumer type implementing . /// A configurator that allows configuring the consumer's receive endpoint. - new ITransportConsumerConfigurator Consumer() + IMessagingTransportConsumerDescriptor Consumer() where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs index 2a8a15c2797..e193c201dbc 100644 --- a/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.InMemory/Descriptors/InMemoryMessagingTransportDescriptor.cs @@ -109,23 +109,23 @@ public InMemoryMessagingTransportDescriptor(IMessagingSetupContext discoveryCont } /// - public new ITransportHandlerConfigurator Handler() + public IMessagingTransportHandlerDescriptor Handler() where THandler : class, IHandler { var name = Context.Naming.GetReceiveEndpointName(typeof(THandler), ReceiveEndpointKind.Default); var endpoint = Endpoint(name); endpoint.Handler(typeof(THandler)); - return new TransportHandlerConfigurator(endpoint); + return new MessagingTransportHandlerDescriptor(endpoint); } /// - public new ITransportConsumerConfigurator Consumer() + public IMessagingTransportConsumerDescriptor Consumer() where TConsumer : class, IConsumer { var name = Context.Naming.GetReceiveEndpointName(typeof(TConsumer), ReceiveEndpointKind.Default); var endpoint = Endpoint(name); endpoint.Consumer(typeof(TConsumer)); - return new TransportConsumerConfigurator(endpoint); + return new MessagingTransportConsumerDescriptor(endpoint); } /// diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs index f4e477b34d5..bd1f092da06 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/IPostgresMessagingTransportDescriptor.cs @@ -102,11 +102,11 @@ public interface IPostgresMessagingTransportDescriptor string? before = null, string? after = null); - /// - new ITransportHandlerConfigurator Handler() + /// Claims a handler for this transport, creating a convention-named endpoint. + IMessagingTransportHandlerDescriptor Handler() where THandler : class, IHandler; - /// - new ITransportConsumerConfigurator Consumer() + /// Claims a consumer for this transport, creating a convention-named endpoint. + IMessagingTransportConsumerDescriptor Consumer() where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs index 8ebdbd79921..50e1c8b8526 100644 --- a/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.Postgres/Descriptors/PostgresMessagingTransportDescriptor.cs @@ -109,23 +109,23 @@ public PostgresMessagingTransportDescriptor(IMessagingSetupContext discoveryCont } /// - public new ITransportHandlerConfigurator Handler() + public IMessagingTransportHandlerDescriptor Handler() where THandler : class, IHandler { var name = Context.Naming.GetReceiveEndpointName(typeof(THandler), ReceiveEndpointKind.Default); var endpoint = Endpoint(name); endpoint.Handler(typeof(THandler)); - return new TransportHandlerConfigurator(endpoint); + return new MessagingTransportHandlerDescriptor(endpoint); } /// - public new ITransportConsumerConfigurator Consumer() + public IMessagingTransportConsumerDescriptor Consumer() where TConsumer : class, IConsumer { var name = Context.Naming.GetReceiveEndpointName(typeof(TConsumer), ReceiveEndpointKind.Default); var endpoint = Endpoint(name); endpoint.Consumer(typeof(TConsumer)); - return new TransportConsumerConfigurator(endpoint); + return new MessagingTransportConsumerDescriptor(endpoint); } /// diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs index 637463dd430..ed05a8cb727 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQMessagingTransportDescriptor.cs @@ -102,11 +102,11 @@ IRabbitMQMessagingTransportDescriptor ConnectionProvider( string? before = null, string? after = null); - /// - new ITransportHandlerConfigurator Handler() + /// Claims a handler for this transport, creating a convention-named endpoint. + IMessagingTransportHandlerDescriptor Handler() where THandler : class, IHandler; - /// - new ITransportConsumerConfigurator Consumer() + /// Claims a consumer for this transport, creating a convention-named endpoint. + IMessagingTransportConsumerDescriptor Consumer() where TConsumer : class, IConsumer; } diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs index a8f33de54b3..c977aed4b40 100644 --- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/RabbitMQMessagingTransportDescriptor.cs @@ -103,23 +103,23 @@ public RabbitMQMessagingTransportDescriptor(IMessagingSetupContext discoveryCont } /// - public new ITransportHandlerConfigurator Handler() + public IMessagingTransportHandlerDescriptor Handler() where THandler : class, IHandler { var name = Context.Naming.GetReceiveEndpointName(typeof(THandler), ReceiveEndpointKind.Default); var endpoint = Endpoint(name); endpoint.Handler(typeof(THandler)); - return new TransportHandlerConfigurator(endpoint); + return new MessagingTransportHandlerDescriptor(endpoint); } /// - public new ITransportConsumerConfigurator Consumer() + public IMessagingTransportConsumerDescriptor Consumer() where TConsumer : class, IConsumer { var name = Context.Naming.GetReceiveEndpointName(typeof(TConsumer), ReceiveEndpointKind.Default); var endpoint = Endpoint(name); endpoint.Consumer(typeof(TConsumer)); - return new TransportConsumerConfigurator(endpoint); + return new MessagingTransportConsumerDescriptor(endpoint); } /// diff --git a/src/Mocha/src/Mocha/Transport/ITransportConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/IMessagingTransportConsumerDescriptor.cs similarity index 76% rename from src/Mocha/src/Mocha/Transport/ITransportConsumerConfigurator.cs rename to src/Mocha/src/Mocha/Transport/IMessagingTransportConsumerDescriptor.cs index 486afafc795..bf400c4558b 100644 --- a/src/Mocha/src/Mocha/Transport/ITransportConsumerConfigurator.cs +++ b/src/Mocha/src/Mocha/Transport/IMessagingTransportConsumerDescriptor.cs @@ -5,7 +5,7 @@ namespace Mocha; /// Allows configuring the receive endpoint for a claimed consumer. /// /// The endpoint descriptor type exposed by the transport. -public interface ITransportConsumerConfigurator +public interface IMessagingTransportConsumerDescriptor { /// /// Configures the receive endpoint for this consumer. @@ -13,5 +13,5 @@ public interface ITransportConsumerConfigurator /// /// A delegate to configure the endpoint descriptor. /// This configurator for method chaining. - ITransportConsumerConfigurator ConfigureEndpoint(Action configure); + IMessagingTransportConsumerDescriptor ConfigureEndpoint(Action configure); } diff --git a/src/Mocha/src/Mocha/Transport/ITransportHandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/IMessagingTransportHandlerDescriptor.cs similarity index 76% rename from src/Mocha/src/Mocha/Transport/ITransportHandlerConfigurator.cs rename to src/Mocha/src/Mocha/Transport/IMessagingTransportHandlerDescriptor.cs index b01fe713aaf..80e66b875d1 100644 --- a/src/Mocha/src/Mocha/Transport/ITransportHandlerConfigurator.cs +++ b/src/Mocha/src/Mocha/Transport/IMessagingTransportHandlerDescriptor.cs @@ -5,7 +5,7 @@ namespace Mocha; /// Allows configuring the receive endpoint for a claimed handler. /// /// The endpoint descriptor type exposed by the transport. -public interface ITransportHandlerConfigurator +public interface IMessagingTransportHandlerDescriptor { /// /// Configures the receive endpoint for this handler. @@ -13,5 +13,5 @@ public interface ITransportHandlerConfigurator /// /// A delegate to configure the endpoint descriptor. /// This configurator for method chaining. - ITransportHandlerConfigurator ConfigureEndpoint(Action configure); + IMessagingTransportHandlerDescriptor ConfigureEndpoint(Action configure); } diff --git a/src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportConsumerDescriptor.cs similarity index 50% rename from src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs rename to src/Mocha/src/Mocha/Transport/MessagingTransportConsumerDescriptor.cs index 5e7c433ae0b..304b5fb6bd8 100644 --- a/src/Mocha/src/Mocha/Transport/TransportHandlerConfigurator.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportConsumerDescriptor.cs @@ -1,15 +1,15 @@ namespace Mocha; /// -/// Internal implementation of that +/// Internal implementation of that /// passes endpoint configuration actions directly to the underlying endpoint descriptor. /// /// The endpoint descriptor type exposed by the transport. -internal sealed class TransportHandlerConfigurator(TEndpointDescriptor endpoint) - : ITransportHandlerConfigurator +internal sealed class MessagingTransportConsumerDescriptor(TEndpointDescriptor endpoint) + : IMessagingTransportConsumerDescriptor { /// - public ITransportHandlerConfigurator ConfigureEndpoint( + public IMessagingTransportConsumerDescriptor ConfigureEndpoint( Action configure) { configure(endpoint); diff --git a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs index c6365bcbf9b..3243dbbf7e7 100644 --- a/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportDescriptor.cs @@ -90,24 +90,6 @@ IMessagingTransportDescriptor UseReceive( ReceiveMiddlewareConfiguration configuration, string? before = null, string? after = null); - - /// - /// Claims a handler for this transport and returns a configurator for its receive endpoint. - /// The handler will be bound to a convention-named endpoint on this transport during initialization. - /// - /// The handler type implementing . - /// A configurator that allows configuring the handler's receive endpoint. - ITransportHandlerConfigurator> Handler() - where THandler : class, IHandler; - - /// - /// Claims a consumer for this transport and returns a configurator for its receive endpoint. - /// The consumer will be bound to a convention-named endpoint on this transport during initialization. - /// - /// The consumer type implementing . - /// A configurator that allows configuring the consumer's receive endpoint. - ITransportConsumerConfigurator> Consumer() - where TConsumer : class, IConsumer; } /// @@ -227,16 +209,6 @@ public IMessagingTransportDescriptor UseReceive( return this; } - /// - public virtual ITransportHandlerConfigurator> - Handler() where THandler : class, IHandler - => throw new NotSupportedException("Use the transport-specific Handler() method."); - - /// - public virtual ITransportConsumerConfigurator> - Consumer() where TConsumer : class, IConsumer - => throw new NotSupportedException("Use the transport-specific Consumer() method."); - /// /// Returns this descriptor as an extension point for the transport configuration, allowing additional /// configuration to be layered by external modules. diff --git a/src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs b/src/Mocha/src/Mocha/Transport/MessagingTransportHandlerDescriptor.cs similarity index 50% rename from src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs rename to src/Mocha/src/Mocha/Transport/MessagingTransportHandlerDescriptor.cs index 11ed6000a13..8637df8073c 100644 --- a/src/Mocha/src/Mocha/Transport/TransportConsumerConfigurator.cs +++ b/src/Mocha/src/Mocha/Transport/MessagingTransportHandlerDescriptor.cs @@ -1,15 +1,15 @@ namespace Mocha; /// -/// Internal implementation of that +/// Internal implementation of that /// passes endpoint configuration actions directly to the underlying endpoint descriptor. /// /// The endpoint descriptor type exposed by the transport. -internal sealed class TransportConsumerConfigurator(TEndpointDescriptor endpoint) - : ITransportConsumerConfigurator +internal sealed class MessagingTransportHandlerDescriptor(TEndpointDescriptor endpoint) + : IMessagingTransportHandlerDescriptor { /// - public ITransportConsumerConfigurator ConfigureEndpoint( + public IMessagingTransportHandlerDescriptor ConfigureEndpoint( Action configure) { configure(endpoint); From d824436b9b518062a9ab337d2384c699f4519051 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 18:47:57 +0000 Subject: [PATCH 4/7] cleanup --- .../InMemoryHandlerClaimTests.cs | 141 +++++++++++++++++ .../PostgresHandlerClaimTests.cs | 149 ++++++++++++++++++ .../RabbitMQHandlerClaimTests.cs | 136 ++++++++++++++++ .../docs/mocha/v1/routing-and-endpoints.md | 17 ++ .../src/docs/mocha/v1/transports/in-memory.md | 45 ++++++ website/src/docs/mocha/v1/transports/index.md | 43 ++++- .../src/docs/mocha/v1/transports/rabbitmq.md | 24 ++- 7 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs create mode 100644 src/Mocha/test/Mocha.Transport.Postgres.Tests/PostgresHandlerClaimTests.cs create mode 100644 src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQHandlerClaimTests.cs diff --git a/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs b/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs new file mode 100644 index 00000000000..c129e3806bb --- /dev/null +++ b/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs @@ -0,0 +1,141 @@ +using Microsoft.Extensions.DependencyInjection; +using Mocha.Transport.InMemory.Tests.Helpers; + +namespace Mocha.Transport.InMemory.Tests; + +public class InMemoryHandlerClaimTests +{ + [Fact] + public void Handler_Should_CreateEndpoint_When_Called() + { + // arrange & act + var runtime = new ServiceCollection() + .AddMessageBus() + .AddEventHandler() + .AddInMemory(t => + { + t.BindHandlersExplicitly(); + t.Handler(); + }) + .BuildRuntime(); + var transport = runtime.Transports.OfType().Single(); + + // assert + var endpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "order-created"); + + Assert.NotNull(endpoint); + Assert.Contains(typeof(OrderCreatedHandler), endpoint.Configuration.ConsumerIdentities); + } + + [Fact] + public void Handler_Should_ApplyConfig_When_ConfigureEndpointCalled() + { + // arrange & act + var runtime = new ServiceCollection() + .AddMessageBus() + .AddEventHandler() + .AddInMemory(t => + { + t.BindHandlersExplicitly(); + t.Handler() + .ConfigureEndpoint(e => e.MaxConcurrency(5)); + }) + .BuildRuntime(); + var transport = runtime.Transports.OfType().Single(); + + // assert + var endpoint = transport.ReceiveEndpoints + .OfType() + .Single(e => e.Name == "order-created"); + + Assert.Equal(5, endpoint.Configuration.MaxConcurrency); + } + + [Fact] + public void Consumer_Should_CreateEndpoint_When_Called() + { + // arrange & act + var runtime = new ServiceCollection() + .AddMessageBus() + .AddConsumer() + .AddInMemory(t => + { + t.BindHandlersExplicitly(); + t.Consumer(); + }) + .BuildRuntime(); + var transport = runtime.Transports.OfType().Single(); + + // assert + var endpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "test-order"); + + Assert.NotNull(endpoint); + Assert.Contains(typeof(TestOrderConsumer), endpoint.Configuration.ConsumerIdentities); + } + + [Fact] + public void Handler_Should_MergeWithExisting_When_ConventionNameMatchesExplicitEndpoint() + { + // arrange & act + var runtime = new ServiceCollection() + .AddMessageBus() + .AddEventHandler() + .AddInMemory(t => + { + t.BindHandlersExplicitly(); + t.Endpoint("order-created").MaxConcurrency(10); + t.Handler(); + }) + .BuildRuntime(); + var transport = runtime.Transports.OfType().Single(); + + // assert - only one endpoint with that name, containing both the handler and the concurrency setting + var endpoints = transport.ReceiveEndpoints + .OfType() + .Where(e => e.Name == "order-created") + .ToList(); + + var endpoint = Assert.Single(endpoints); + Assert.Contains(typeof(OrderCreatedHandler), endpoint.Configuration.ConsumerIdentities); + Assert.Equal(10, endpoint.Configuration.MaxConcurrency); + } + + [Fact] + public void Handler_Should_CreateSeparateEndpoints_When_MultipleHandlersClaimed() + { + // arrange & act + var runtime = new ServiceCollection() + .AddMessageBus() + .AddEventHandler() + .AddEventHandler() + .AddInMemory(t => + { + t.BindHandlersExplicitly(); + t.Handler(); + t.Handler(); + }) + .BuildRuntime(); + var transport = runtime.Transports.OfType().Single(); + + // assert + var endpointNames = transport.ReceiveEndpoints + .OfType() + .Where(e => e.Name == "order-created" || e.Name == "order-created-handler-2") + .Select(e => e.Name) + .OrderBy(n => n) + .ToList(); + + Assert.Equal(2, endpointNames.Count); + Assert.Contains("order-created", endpointNames); + Assert.Contains("order-created-handler-2", endpointNames); + } + + public sealed class TestOrderConsumer : IConsumer + { + public ValueTask ConsumeAsync(IConsumeContext context) => default; + } +} diff --git a/src/Mocha/test/Mocha.Transport.Postgres.Tests/PostgresHandlerClaimTests.cs b/src/Mocha/test/Mocha.Transport.Postgres.Tests/PostgresHandlerClaimTests.cs new file mode 100644 index 00000000000..02d59cd5982 --- /dev/null +++ b/src/Mocha/test/Mocha.Transport.Postgres.Tests/PostgresHandlerClaimTests.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.DependencyInjection; +using Mocha.Transport.Postgres.Tests.Helpers; + +namespace Mocha.Transport.Postgres.Tests; + +public class PostgresHandlerClaimTests +{ + [Fact] + public void Handler_Should_CreateEndpoint_When_Called() + { + // arrange & act + var runtime = CreateRuntime( + b => b.AddEventHandler(), + t => t.Handler()); + var transport = runtime.Transports.OfType().Single(); + + // assert - convention name for OrderCreatedHandler is "order-created" + var endpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "order-created"); + + Assert.NotNull(endpoint); + Assert.Equal(ReceiveEndpointKind.Default, endpoint!.Kind); + } + + [Fact] + public void Handler_Should_ApplyConfig_When_ConfigureEndpointCalled() + { + // arrange & act + var runtime = CreateRuntime( + b => b.AddEventHandler(), + t => t.Handler() + .ConfigureEndpoint(e => e.Queue("custom-handler-queue"))); + var transport = runtime.Transports.OfType().Single(); + + // assert - the endpoint should exist with the custom queue name + var endpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "order-created"); + + Assert.NotNull(endpoint); + Assert.Equal("custom-handler-queue", endpoint!.Queue.Name); + } + + [Fact] + public void Consumer_Should_CreateEndpoint_When_Called() + { + // arrange & act + var runtime = CreateRuntime( + b => b.AddConsumer(), + t => t.Consumer()); + var transport = runtime.Transports.OfType().Single(); + + // assert - convention name for OrderSpyConsumer is "order-spy" + var endpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "order-spy"); + + Assert.NotNull(endpoint); + Assert.Equal(ReceiveEndpointKind.Default, endpoint!.Kind); + } + + [Fact] + public void Handler_Should_MergeWithExisting_When_ConventionNameMatchesExplicitEndpoint() + { + // arrange & act - "order-created" is the convention name for OrderCreatedHandler; + // creating an explicit endpoint with the same name first should merge. + var runtime = CreateRuntime( + b => b.AddEventHandler(), + t => + { + t.Endpoint("order-created").Queue("merged-queue"); + t.Handler(); + }); + var transport = runtime.Transports.OfType().Single(); + + // assert - should be exactly one endpoint with that name, not two + var endpoints = transport.ReceiveEndpoints + .OfType() + .Where(e => e.Name == "order-created") + .ToList(); + + Assert.Single(endpoints); + Assert.Equal("merged-queue", endpoints[0].Queue.Name); + } + + [Fact] + public void Handler_Should_CreateSeparateEndpoints_When_MultipleHandlersClaimed() + { + // arrange & act + var runtime = CreateRuntime( + b => + { + b.AddEventHandler(); + b.AddEventHandler(); + }, + t => + { + t.Handler(); + t.Handler(); + }); + var transport = runtime.Transports.OfType().Single(); + + // assert - two distinct endpoints for the two handlers + var orderEndpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "order-created"); + var paymentEndpoint = transport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "payment-received"); + + Assert.NotNull(orderEndpoint); + Assert.NotNull(paymentEndpoint); + Assert.NotSame(orderEndpoint, paymentEndpoint); + } + + private static MessagingRuntime CreateRuntime( + Action configureBuilder, + Action configureTransport) + { + var services = new ServiceCollection(); + services.AddSingleton(new MessageRecorder()); + var builder = services.AddMessageBus(); + configureBuilder(builder); + var runtime = builder + .AddPostgres(t => + { + t.ConnectionString("Host=localhost;Database=mocha_test;Username=test;Password=test"); + configureTransport(t); + }) + .BuildRuntime(); + return runtime; + } + + public sealed class OrderSpyConsumer : IConsumer + { + public ValueTask ConsumeAsync(IConsumeContext context) => default; + } + + public sealed class PaymentReceived + { + public required string PaymentId { get; init; } + } + + public sealed class PaymentReceivedHandler : IEventHandler + { + public ValueTask HandleAsync(PaymentReceived message, CancellationToken cancellationToken) => default; + } +} diff --git a/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQHandlerClaimTests.cs b/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQHandlerClaimTests.cs new file mode 100644 index 00000000000..f89266dd2b0 --- /dev/null +++ b/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQHandlerClaimTests.cs @@ -0,0 +1,136 @@ +using Microsoft.Extensions.DependencyInjection; +using Mocha.Transport.RabbitMQ.Tests.Helpers; + +namespace Mocha.Transport.RabbitMQ.Tests; + +public class RabbitMQHandlerClaimTests +{ + [Fact] + public void Handler_Should_CreateEndpoint_When_Called() + { + // arrange + var runtime = CreateRuntime( + b => b.AddEventHandler(), + t => t.Handler()); + var transport = runtime.Transports.OfType().Single(); + + // act + var endpoint = transport.ReceiveEndpoints.OfType().First(e => e.Name == "order-created"); + + // assert + Assert.Contains(typeof(OrderCreatedHandler), endpoint.Configuration.ConsumerIdentities); + } + + [Fact] + public void Handler_Should_ApplyConfig_When_ConfigureEndpointCalled() + { + // arrange + var runtime = CreateRuntime( + b => b.AddEventHandler(), + t => t.Handler() + .ConfigureEndpoint(e => e.MaxPrefetch(50).MaxConcurrency(5))); + var transport = runtime.Transports.OfType().Single(); + + // act + var endpoint = transport.ReceiveEndpoints.OfType().First(e => e.Name == "order-created"); + + // assert + Assert.Equal(50, endpoint.Configuration.MaxPrefetch); + Assert.Equal(5, endpoint.Configuration.MaxConcurrency); + } + + [Fact] + public void Consumer_Should_CreateEndpoint_When_Called() + { + // arrange + var runtime = CreateRuntime( + b => b.AddConsumer(), + t => t.Consumer()); + var transport = runtime.Transports.OfType().Single(); + + // act + var endpoint = transport.ReceiveEndpoints.OfType().First(e => e.Name == "order-spy"); + + // assert + Assert.Contains(typeof(OrderSpyConsumer), endpoint.Configuration.ConsumerIdentities); + } + + [Fact] + public void Handler_Should_MergeWithExisting_When_ConventionNameMatchesExplicitEndpoint() + { + // arrange + var runtime = CreateRuntime( + b => b.AddEventHandler(), + t => + { + t.Endpoint("order-created").MaxConcurrency(10); + t.Handler(); + }); + var transport = runtime.Transports.OfType().Single(); + + // act + var endpoints = transport.ReceiveEndpoints + .OfType() + .Where(e => e.Name == "order-created") + .ToList(); + + // assert + Assert.Single(endpoints); + Assert.Contains(typeof(OrderCreatedHandler), endpoints[0].Configuration.ConsumerIdentities); + Assert.Equal(10, endpoints[0].Configuration.MaxConcurrency); + } + + [Fact] + public void Handler_Should_CreateSeparateEndpoints_When_MultipleHandlersClaimed() + { + // arrange + var runtime = CreateRuntime( + b => + { + b.AddEventHandler(); + b.AddRequestHandler(); + }, + t => + { + t.Handler(); + t.Handler(); + }); + var transport = runtime.Transports.OfType().Single(); + + // act + var orderEndpoint = transport.ReceiveEndpoints + .OfType() + .FirstOrDefault(e => e.Name == "order-created"); + var statusEndpoint = transport.ReceiveEndpoints + .OfType() + .FirstOrDefault(e => e.Name == "get-order-status"); + + // assert + Assert.NotNull(orderEndpoint); + Assert.NotNull(statusEndpoint); + Assert.NotEqual(orderEndpoint, statusEndpoint); + } + + private static MessagingRuntime CreateRuntime( + Action configureBuilder, + Action configureTransport) + { + var services = new ServiceCollection(); + services.AddSingleton(new MessageRecorder()); + var builder = services.AddMessageBus(); + configureBuilder(builder); + var runtime = builder + .AddRabbitMQ(t => + { + t.ConnectionProvider(_ => new StubConnectionProvider()); + configureTransport(t); + }) + .BuildRuntime(); + return runtime; + } + + public sealed class OrderSpyConsumer : IConsumer + { + public ValueTask ConsumeAsync(IConsumeContext context) => default; + } +} diff --git a/website/src/docs/mocha/v1/routing-and-endpoints.md b/website/src/docs/mocha/v1/routing-and-endpoints.md index 8cf1429d94d..f3b7821d913 100644 --- a/website/src/docs/mocha/v1/routing-and-endpoints.md +++ b/website/src/docs/mocha/v1/routing-and-endpoints.md @@ -344,6 +344,23 @@ builder.Services Use `transport.Endpoint("name")` when you need to control the endpoint name or bind multiple handlers to the same endpoint. Use `transport.Handler()` when you want the convention-derived name and are configuring a single handler's endpoint. +## Multi-transport handler routing + +In a multi-transport setup, `Handler()` also determines which transport owns the handler. Mark one transport as the default with `.IsDefaultTransport()`, then claim specific handlers on other transports: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddEventHandler() + .AddRabbitMQ(r => r.IsDefaultTransport()) // default for unclaimed handlers + .AddInMemory(m => m.Handler()); // AuditHandler claimed by InMemory +// OrderPlacedHandler → RabbitMQ (default, implicit) +// AuditHandler → InMemory (claimed) +``` + +A claimed handler is bound to the claiming transport regardless of which transport is the default. Unclaimed handlers fall through to the default transport. This is the recommended pattern for multi-transport routing — it avoids `BindHandlersExplicitly()` and keeps the configuration minimal. + For outbound endpoints: ```csharp diff --git a/website/src/docs/mocha/v1/transports/in-memory.md b/website/src/docs/mocha/v1/transports/in-memory.md index 4e9d162a2a1..37b8c2e47fb 100644 --- a/website/src/docs/mocha/v1/transports/in-memory.md +++ b/website/src/docs/mocha/v1/transports/in-memory.md @@ -70,6 +70,51 @@ The following shows the default topology Mocha creates when you register an even +# Configure handler endpoints + +Use `transport.Handler()` to claim a handler for the InMemory transport and configure its convention-named endpoint: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddInMemory(transport => + { + transport.Handler() + .ConfigureEndpoint(e => e.MaxConcurrency(5)); + }); +``` + +The handler keeps its convention-derived endpoint name. `ConfigureEndpoint()` can be called multiple times - actions compose in declaration order: + +```csharp +transport.Handler() + .ConfigureEndpoint(e => e.MaxConcurrency(5)) + .ConfigureEndpoint(e => e.FaultEndpoint("order-errors")); +``` + +Inside `ConfigureEndpoint()`, you have access to the full `IInMemoryReceiveEndpointDescriptor`, which supports `MaxConcurrency()`, `Queue()`, `FaultEndpoint()`, `SkippedEndpoint()`, and receive middleware. + +For raw `IConsumer` types, use `transport.Consumer()`: + +```csharp +transport.Consumer() + .ConfigureEndpoint(e => e.MaxConcurrency(3)); +``` + +In a multi-transport setup, `Handler()` also claims the handler for this transport, overriding the default: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddEventHandler() + .AddRabbitMQ(r => r.IsDefaultTransport()) + .AddInMemory(m => m.Handler()); +// OrderPlacedEventHandler → RabbitMQ (default) +// AuditHandler → InMemory (claimed) +``` + # Declare custom topology The InMemory transport auto-generates topology from your handler registrations. To declare custom topology explicitly: diff --git a/website/src/docs/mocha/v1/transports/index.md b/website/src/docs/mocha/v1/transports/index.md index 2bd317418f8..0de535c20f8 100644 --- a/website/src/docs/mocha/v1/transports/index.md +++ b/website/src/docs/mocha/v1/transports/index.md @@ -119,9 +119,48 @@ builder.Services Explicit binding is useful when you need multiple handlers on the same queue, custom queue names, or fine-grained control over endpoint topology. +# Claim handlers for a transport + +When you need to configure a handler's endpoint without switching to fully explicit binding, use `transport.Handler()`. This claims the handler for the transport and returns a descriptor that lets you configure the endpoint through `ConfigureEndpoint()`: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddRabbitMQ(transport => + { + transport.Handler() + .ConfigureEndpoint(e => e.MaxPrefetch(50).MaxConcurrency(10)); + }); +``` + +The handler still gets a convention-named endpoint - `Handler()` does not change the name. It gives you a handle to configure that endpoint without needing `BindHandlersExplicitly()` or knowing the endpoint name. + +For raw `IConsumer` types, the equivalent is `transport.Consumer()`: + +```csharp +transport.Consumer() + .ConfigureEndpoint(e => e.MaxConcurrency(3)); +``` + +`Handler()` and `Consumer()` are the primary tool for multi-transport routing. When a handler is claimed by a transport, it is bound to that transport regardless of which transport is marked as the default. + # Use multiple transports -You can register multiple transports and route specific handlers to specific transports. The first transport registered is the default. Use the transport configuration callback to assign handlers: +You can register multiple transports and route specific handlers to specific transports. Mark one transport as the default with `.IsDefaultTransport()`. Any handler not explicitly claimed by another transport is bound to the default: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddEventHandler() + .AddRabbitMQ(r => r.IsDefaultTransport()) // default for unclaimed handlers + .AddInMemory(m => m.Handler()); // AuditHandler claimed by InMemory +// OrderPlacedEventHandler → RabbitMQ (default, implicit) +// AuditHandler → InMemory (claimed) +``` + +You can also use the older `Endpoint("name").Handler()` pattern with explicit binding: ```csharp builder.Services @@ -133,6 +172,8 @@ builder.Services // High-throughput transport for click-stream data .AddInMemory(transport => { + transport.BindHandlersExplicitly(); + transport.Endpoint("click-stream") .Handler(); }); diff --git a/website/src/docs/mocha/v1/transports/rabbitmq.md b/website/src/docs/mocha/v1/transports/rabbitmq.md index 84815b67154..f1c83bdaa2e 100644 --- a/website/src/docs/mocha/v1/transports/rabbitmq.md +++ b/website/src/docs/mocha/v1/transports/rabbitmq.md @@ -390,7 +390,29 @@ transport.DeclareBinding("platform-events", "my-queue"); // auto-provisioned (de # Prefetch and concurrency -Customize queue names, prefetch counts, and handler assignments on receive endpoints: +Use `transport.Handler()` to claim a handler and configure prefetch and concurrency on its convention-named endpoint: + +```csharp +builder.Services + .AddMessageBus() + .AddEventHandler() + .AddRabbitMQ(transport => + { + transport.Handler() + .ConfigureEndpoint(e => e.MaxPrefetch(50).MaxConcurrency(10)); + }); +``` + +This keeps the convention-derived endpoint name while tuning the consumer settings. `ConfigureEndpoint()` can be called multiple times - actions compose in declaration order: + +```csharp +transport.Handler() + .ConfigureEndpoint(e => e.MaxPrefetch(50)) + .ConfigureEndpoint(e => e.MaxConcurrency(10)) + .ConfigureEndpoint(e => e.FaultEndpoint("order-errors")); +``` + +For full control over the endpoint name and queue, use explicit binding with `Endpoint("name")`: ```csharp builder.Services From 698907a16f21c782c44114030d05d5f51b2c17fa Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 18:48:11 +0000 Subject: [PATCH 5/7] cleanup --- .../InMemoryHandlerClaimTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs b/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs index c129e3806bb..fda912fd7c4 100644 --- a/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs +++ b/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs @@ -134,6 +134,62 @@ public void Handler_Should_CreateSeparateEndpoints_When_MultipleHandlersClaimed( Assert.Contains("order-created-handler-2", endpointNames); } + [Fact] + public void Handler_Should_BindToNonDefaultTransport_When_ClaimedByInMemory() + { + // arrange & act + var runtime = new ServiceCollection() + .AddMessageBus() + .AddEventHandler() + .AddEventHandler() + // Default transport — unclaimed handlers bind here automatically. + .AddInMemory(t => + { + t.Name("default"); + t.IsDefaultTransport(); + }) + // Non-default transport — only explicitly claimed handlers bind here. + .AddInMemory(t => + { + t.Name("inmemory"); + t.BindHandlersExplicitly(); + t.Handler(); + }) + .BuildRuntime(); + + var defaultTransport = runtime.Transports + .OfType() + .Single(t => t.Name == "default"); + + var nonDefaultTransport = runtime.Transports + .OfType() + .Single(t => t.Name == "inmemory"); + + // assert — claimed handler is on the non-default transport + var claimedEndpoint = nonDefaultTransport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name == "order-created"); + + Assert.NotNull(claimedEndpoint); + Assert.Contains(typeof(OrderCreatedHandler), claimedEndpoint.Configuration.ConsumerIdentities); + + // assert — unclaimed handler fell through to the default transport + var unclaimedEndpoint = defaultTransport.ReceiveEndpoints + .OfType() + .SingleOrDefault(e => e.Name.EndsWith("order-created-handler-2")); + + Assert.NotNull(unclaimedEndpoint); + + // assert — neither transport has the other's handler endpoint + Assert.DoesNotContain( + nonDefaultTransport.ReceiveEndpoints.OfType(), + e => e.Name.EndsWith("order-created-handler-2")); + + Assert.DoesNotContain( + defaultTransport.ReceiveEndpoints.OfType(), + e => e.Name == "order-created"); + } + public sealed class TestOrderConsumer : IConsumer { public ValueTask ConsumeAsync(IConsumeContext context) => default; From b1fa209ac45867feb005b138d0c6824962dfb92a Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 18:52:02 +0000 Subject: [PATCH 6/7] cleanup --- .../Internal/PoolingBenchmarks.cs | 2 +- .../src/Examples/MediatorShowcase/Handlers.cs | 4 +-- .../MediatorShowcase/MediatorShowcase.cs | 2 +- .../Examples/Transports/RabbitMQ/RabbitMQ.cs | 2 +- ...iatorMiddlewareFactoryContextExtensions.cs | 2 +- .../NotificationStrategyTests.cs | 2 +- .../InMemoryHandlerClaimTests.cs | 10 +++---- website/src/docs/mocha/v1/diagnostics.md | 28 +++++++++---------- .../src/docs/mocha/v1/handler-registration.md | 2 +- website/src/docs/mocha/v1/hosting.md | 10 +++---- website/src/docs/mocha/v1/mediator/index.md | 12 ++++---- .../v1/mediator/pipeline-and-middleware.md | 4 +-- .../docs/mocha/v1/routing-and-endpoints.md | 2 +- 13 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs index 9c8b7c03fed..e74c3b7806f 100644 --- a/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs +++ b/src/Mocha/benchmarks/Mocha.Mediator.Benchmarks/Internal/PoolingBenchmarks.cs @@ -73,7 +73,7 @@ public void Cleanup() obj.Message = s_message; obj.MessageType = s_type; var result = obj.Message; - // No return — GC collects it + // No return - GC collects it return result; } diff --git a/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs b/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs index e6767120a1e..2ee9d154a9a 100644 --- a/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs +++ b/src/Mocha/src/Examples/MediatorShowcase/Handlers.cs @@ -122,7 +122,7 @@ public sealed class OrderShippedEmailHandler(ILogger l { public ValueTask HandleAsync(OrderShippedNotification notification, CancellationToken cancellationToken) { - logger.LogInformation("[Email] Order {OrderId} shipped — email sent to customer", notification.OrderId); + logger.LogInformation("[Email] Order {OrderId} shipped - email sent to customer", notification.OrderId); return ValueTask.CompletedTask; } } @@ -135,7 +135,7 @@ public sealed class OrderShippedAnalyticsHandler(ILogger { await sender.SendAsync(new CreateProductCommand(req.Name, req.Price)); diff --git a/src/Mocha/src/Examples/Transports/RabbitMQ/RabbitMQ.cs b/src/Mocha/src/Examples/Transports/RabbitMQ/RabbitMQ.cs index 908cd48e969..4383d691306 100644 --- a/src/Mocha/src/Examples/Transports/RabbitMQ/RabbitMQ.cs +++ b/src/Mocha/src/Examples/Transports/RabbitMQ/RabbitMQ.cs @@ -46,7 +46,7 @@ .Handler(); // Declare a quorum queue explicitly with durable flag. - // Quorum queues require durable=true — non-durable quorum queues are not supported. + // Quorum queues require durable=true - non-durable quorum queues are not supported. transport.DeclareQueue("orders.processing") .Durable() .AutoProvision() diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs index 8d5bfa72fbb..0605707f264 100644 --- a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs +++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs @@ -4,7 +4,7 @@ namespace Mocha.Mediator; /// Extension methods for that allow middleware /// factories to inspect the pipeline being compiled and decide whether to participate. /// Returning next from a middleware factory when these checks fail eliminates the -/// middleware from that pipeline entirely — zero runtime cost. +/// middleware from that pipeline entirely - zero runtime cost. /// public static class MediatorMiddlewareFactoryContextExtensions { diff --git a/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs b/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs index bbd0f582004..7c3f1a592c1 100644 --- a/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs +++ b/src/Mocha/test/Mocha.Mediator.Tests/NotificationStrategyTests.cs @@ -143,7 +143,7 @@ public async Task PublishAsync_Should_PropagateException_When_TaskWhenAllHandler using var scope = provider.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); - // Act & Assert — concurrent mode surfaces AggregateException with all failures + // Act & Assert - concurrent mode surfaces AggregateException with all failures var ex = await Assert.ThrowsAsync( () => mediator.PublishAsync(new StrategyTestNotification("throw")).AsTask()); diff --git a/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs b/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs index fda912fd7c4..c12721335a4 100644 --- a/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs +++ b/src/Mocha/test/Mocha.Transport.InMemory.Tests/InMemoryHandlerClaimTests.cs @@ -142,13 +142,13 @@ public void Handler_Should_BindToNonDefaultTransport_When_ClaimedByInMemory() .AddMessageBus() .AddEventHandler() .AddEventHandler() - // Default transport — unclaimed handlers bind here automatically. + // Default transport - unclaimed handlers bind here automatically. .AddInMemory(t => { t.Name("default"); t.IsDefaultTransport(); }) - // Non-default transport — only explicitly claimed handlers bind here. + // Non-default transport - only explicitly claimed handlers bind here. .AddInMemory(t => { t.Name("inmemory"); @@ -165,7 +165,7 @@ public void Handler_Should_BindToNonDefaultTransport_When_ClaimedByInMemory() .OfType() .Single(t => t.Name == "inmemory"); - // assert — claimed handler is on the non-default transport + // assert - claimed handler is on the non-default transport var claimedEndpoint = nonDefaultTransport.ReceiveEndpoints .OfType() .SingleOrDefault(e => e.Name == "order-created"); @@ -173,14 +173,14 @@ public void Handler_Should_BindToNonDefaultTransport_When_ClaimedByInMemory() Assert.NotNull(claimedEndpoint); Assert.Contains(typeof(OrderCreatedHandler), claimedEndpoint.Configuration.ConsumerIdentities); - // assert — unclaimed handler fell through to the default transport + // assert - unclaimed handler fell through to the default transport var unclaimedEndpoint = defaultTransport.ReceiveEndpoints .OfType() .SingleOrDefault(e => e.Name.EndsWith("order-created-handler-2")); Assert.NotNull(unclaimedEndpoint); - // assert — neither transport has the other's handler endpoint + // assert - neither transport has the other's handler endpoint Assert.DoesNotContain( nonDefaultTransport.ReceiveEndpoints.OfType(), e => e.Name.EndsWith("order-created-handler-2")); diff --git a/website/src/docs/mocha/v1/diagnostics.md b/website/src/docs/mocha/v1/diagnostics.md index f49249288cc..782e39e1468 100644 --- a/website/src/docs/mocha/v1/diagnostics.md +++ b/website/src/docs/mocha/v1/diagnostics.md @@ -5,7 +5,7 @@ description: "Reference for all compile-time diagnostics emitted by the Mocha so # Diagnostics -Mocha uses a Roslyn source generator to validate your message handlers, consumers, and sagas at compile time. When the generator detects a problem — a missing handler, a duplicate registration, an invalid type — it emits a diagnostic that appears as a compiler warning or error in your IDE and build output. You can fix these issues before your code ever runs. +Mocha uses a Roslyn source generator to validate your message handlers, consumers, and sagas at compile time. When the generator detects a problem - a missing handler, a duplicate registration, an invalid type - it emits a diagnostic that appears as a compiler warning or error in your IDE and build output. You can fix these issues before your code ever runs. # Quick reference @@ -23,7 +23,7 @@ Mocha uses a Roslyn source generator to validate your message handlers, consumer # Mediator diagnostics -These diagnostics apply to the in-process [mediator](/docs/mocha/v1/mediator) — commands, queries, and notifications dispatched within a single process. +These diagnostics apply to the in-process [mediator](/docs/mocha/v1/mediator) - commands, queries, and notifications dispatched within a single process. ## MO0001 @@ -43,7 +43,7 @@ A command or query type is declared but no corresponding handler implementation ```csharp using Mocha.Mediator; -// Command with no handler — triggers MO0001 +// Command with no handler - triggers MO0001 public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; ``` @@ -79,7 +79,7 @@ public class PlaceOrderHandler : ICommandHandler ### Cause -A command or query type has more than one handler implementation. Commands and queries require exactly one handler — the mediator cannot decide which one to call. This diagnostic does not apply to notifications, which support multiple handlers by design. +A command or query type has more than one handler implementation. Commands and queries require exactly one handler - the mediator cannot decide which one to call. This diagnostic does not apply to notifications, which support multiple handlers by design. ### Example @@ -88,7 +88,7 @@ using Mocha.Mediator; public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; -// Two handlers for the same command — triggers MO0002 +// Two handlers for the same command - triggers MO0002 public class PlaceOrderHandler : ICommandHandler { public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) @@ -138,7 +138,7 @@ using Mocha.Mediator; public record GetOrderTotal(Guid OrderId) : IQuery; -// Abstract handler — triggers MO0003 +// Abstract handler - triggers MO0003 public abstract class GetOrderTotalHandler : IQueryHandler { public abstract ValueTask HandleAsync( @@ -185,7 +185,7 @@ A command or query type has unbound type parameters. The mediator dispatches con ```csharp using Mocha.Mediator; -// Open generic command — triggers MO0004 +// Open generic command - triggers MO0004 public record ProcessItem(T Item) : ICommand; ``` @@ -221,7 +221,7 @@ using Mocha.Mediator; public record PlaceOrder(Guid OrderId) : ICommand; public record GetOrder(Guid OrderId) : IQuery; -// Implements both command and query handler — triggers MO0005 +// Implements both command and query handler - triggers MO0005 public class OrderHandler : ICommandHandler, IQueryHandler @@ -259,7 +259,7 @@ public class GetOrderHandler : IQueryHandler # Messaging diagnostics -These diagnostics apply to the [message bus](/docs/mocha/v1/handlers-and-consumers) — event handlers, request handlers, batch handlers, consumers, and sagas that communicate across service boundaries. +These diagnostics apply to the [message bus](/docs/mocha/v1/handlers-and-consumers) - event handlers, request handlers, batch handlers, consumers, and sagas that communicate across service boundaries. ## MO0011 @@ -272,7 +272,7 @@ These diagnostics apply to the [message bus](/docs/mocha/v1/handlers-and-consume ### Cause -A request type (used with `SendAsync` or `RequestAsync`) has more than one [handler](/docs/mocha/v1/handlers-and-consumers) implementation. Request types require exactly one handler — the bus cannot route to multiple targets. +A request type (used with `SendAsync` or `RequestAsync`) has more than one [handler](/docs/mocha/v1/handlers-and-consumers) implementation. Request types require exactly one handler - the bus cannot route to multiple targets. ### Example @@ -281,7 +281,7 @@ using Mocha; public record ProcessPayment(decimal Amount); -// Two handlers for the same request type — triggers MO0011 +// Two handlers for the same request type - triggers MO0011 public class PaymentHandlerA : IEventRequestHandler { public ValueTask HandleAsync( @@ -335,7 +335,7 @@ A messaging handler (`IEventHandler`, `IEventRequestHandler`, `IBatchEvent ```csharp using Mocha; -// Open generic handler — triggers MO0012 +// Open generic handler - triggers MO0012 public class GenericEventHandler : IEventHandler { public ValueTask HandleAsync( @@ -385,7 +385,7 @@ using Mocha; public record OrderPlaced(Guid OrderId); -// Abstract handler — triggers MO0013 +// Abstract handler - triggers MO0013 public abstract class OrderEventHandler : IEventHandler { public abstract ValueTask HandleAsync( @@ -435,7 +435,7 @@ public class RefundSagaState : SagaStateBase public Guid OrderId { get; set; } } -// Constructor requires a parameter — triggers MO0014 +// Constructor requires a parameter - triggers MO0014 public class RefundSaga : Saga { private readonly ILogger _logger; diff --git a/website/src/docs/mocha/v1/handler-registration.md b/website/src/docs/mocha/v1/handler-registration.md index 08183ffde05..5a5514d9b74 100644 --- a/website/src/docs/mocha/v1/handler-registration.md +++ b/website/src/docs/mocha/v1/handler-registration.md @@ -79,7 +79,7 @@ builder.Services .AddRabbitMQ(); ``` -You can mix source-generated and manual registration freely. If both the source generator and manual code register the same handler type, the configurations are composed — the source generator sets up the base registration and your manual call layers additional configuration (such as consumer middleware) on top. +You can mix source-generated and manual registration freely. If both the source generator and manual code register the same handler type, the configurations are composed - the source generator sets up the base registration and your manual call layers additional configuration (such as consumer middleware) on top. > **Prefer the source generator.** Manual registration methods use runtime reflection to create handler consumers. The source generator produces direct, reflection-free factory calls. We guarantee backwards compatibility for the source-generated registration path; the manual registration API is stable at the surface level but its internal behavior may evolve. diff --git a/website/src/docs/mocha/v1/hosting.md b/website/src/docs/mocha/v1/hosting.md index eb3eb309114..c255a951042 100644 --- a/website/src/docs/mocha/v1/hosting.md +++ b/website/src/docs/mocha/v1/hosting.md @@ -13,7 +13,7 @@ dotnet add package Mocha.Hosting # Health checks -Mocha integrates with the [ASP.NET Core health checks](https://learn.microsoft.com/aspnet/core/host-and-monitor/health-checks) system. The health check sends a `HealthRequest` message through the bus and waits for a `HealthResponse`. This verifies the full pipeline — serialization, transport, routing, and handler execution — not just that the broker is reachable. +Mocha integrates with the [ASP.NET Core health checks](https://learn.microsoft.com/aspnet/core/host-and-monitor/health-checks) system. The health check sends a `HealthRequest` message through the bus and waits for a `HealthResponse`. This verifies the full pipeline - serialization, transport, routing, and handler execution - not just that the broker is reachable. ## Register the health check handler @@ -82,7 +82,7 @@ app.MapHealthChecks("/health/live", new() `MapMessageBusDeveloperTopology()` maps an HTTP GET endpoint that returns the runtime bus topology as JSON. This includes all registered handlers, consumers, receive endpoints, dispatch endpoints, and route bindings. -> **Warning:** This endpoint exposes internal details about your messaging infrastructure. Use it only during development — similar to `UseDeveloperExceptionPage`. +> **Warning:** This endpoint exposes internal details about your messaging infrastructure. Use it only during development - similar to `UseDeveloperExceptionPage`. ```csharp using Mocha.Hosting; @@ -113,6 +113,6 @@ The response is a JSON document describing the full bus topology: the host, regi # Next steps -- [Observability](/docs/mocha/v1/observability) — Add OpenTelemetry tracing and metrics to the bus. -- [Reliability](/docs/mocha/v1/reliability) — Configure outbox, inbox, and circuit breakers. -- [Transports](/docs/mocha/v1/transports) — Configure RabbitMQ, InMemory, and multi-transport setups. +- [Observability](/docs/mocha/v1/observability) - Add OpenTelemetry tracing and metrics to the bus. +- [Reliability](/docs/mocha/v1/reliability) - Configure outbox, inbox, and circuit breakers. +- [Transports](/docs/mocha/v1/transports) - Configure RabbitMQ, InMemory, and multi-transport setups. diff --git a/website/src/docs/mocha/v1/mediator/index.md b/website/src/docs/mocha/v1/mediator/index.md index aed5fc124ba..12208631f89 100644 --- a/website/src/docs/mocha/v1/mediator/index.md +++ b/website/src/docs/mocha/v1/mediator/index.md @@ -16,11 +16,11 @@ That registers the mediator infrastructure, discovers your handlers at compile t The mediator sits between your application code and your handlers. Instead of injecting handler interfaces directly, you inject `IMediator` (or `ISender` / `IPublisher`) and dispatch messages through it. The mediator routes each message to the correct handler based on its type. ```csharp -// Without mediator — tight coupling +// Without mediator - tight coupling app.MapPost("/orders", async (PlaceOrderCommandHandler handler) => await handler.HandleAsync(new PlaceOrderCommand(...))); -// With mediator — decoupled dispatch +// With mediator - decoupled dispatch app.MapPost("/orders", async (ISender sender) => await sender.SendAsync(new PlaceOrderCommand(...))); ``` @@ -364,7 +364,7 @@ builder.Services var app = builder.Build(); -// Command — place an order +// Command - place an order app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) => { var result = await sender.SendAsync( @@ -374,11 +374,11 @@ app.MapPost("/orders", async (PlaceOrderRequest request, ISender sender) => : Results.BadRequest(result.Error); }); -// Query — list products +// Query - list products app.MapGet("/products", async (ISender sender) => await sender.QueryAsync(new GetProductsQuery())); -// Notification — broadcast that an order shipped +// Notification - broadcast that an order shipped app.MapPost("/orders/{id}/ship", async (Guid id, IPublisher publisher) => { await publisher.PublishAsync(new OrderShippedNotification(id)); @@ -438,7 +438,7 @@ public sealed class OrderShippedEmailHandler(ILogger l CancellationToken cancellationToken) { logger.LogInformation( - "Order {OrderId} shipped — email sent", notification.OrderId); + "Order {OrderId} shipped - email sent", notification.OrderId); return ValueTask.CompletedTask; } } 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 3d5fa14265a..7935ac4bef1 100644 --- a/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md +++ b/website/src/docs/mocha/v1/mediator/pipeline-and-middleware.md @@ -64,10 +64,10 @@ SendAsync(PlaceOrderCommand) The pipeline is built from two delegate types: ```csharp -// The terminal pipeline delegate — each step in the chain has this shape +// The terminal pipeline delegate - each step in the chain has this shape public delegate ValueTask MediatorDelegate(IMediatorContext context); -// The factory that creates a middleware — runs once per message type at startup +// The factory that creates a middleware - runs once per message type at startup public delegate MediatorDelegate MediatorMiddleware( MediatorMiddlewareFactoryContext context, MediatorDelegate next); diff --git a/website/src/docs/mocha/v1/routing-and-endpoints.md b/website/src/docs/mocha/v1/routing-and-endpoints.md index f3b7821d913..6041bd4ed0b 100644 --- a/website/src/docs/mocha/v1/routing-and-endpoints.md +++ b/website/src/docs/mocha/v1/routing-and-endpoints.md @@ -359,7 +359,7 @@ builder.Services // AuditHandler → InMemory (claimed) ``` -A claimed handler is bound to the claiming transport regardless of which transport is the default. Unclaimed handlers fall through to the default transport. This is the recommended pattern for multi-transport routing — it avoids `BindHandlersExplicitly()` and keeps the configuration minimal. +A claimed handler is bound to the claiming transport regardless of which transport is the default. Unclaimed handlers fall through to the default transport. This is the recommended pattern for multi-transport routing - it avoids `BindHandlersExplicitly()` and keeps the configuration minimal. For outbound endpoints: From a6f1777a5a13403386d40d12770555afc62fe187 Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sun, 29 Mar 2026 19:15:04 +0000 Subject: [PATCH 7/7] fix build --- dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dictionary.txt b/dictionary.txt index 849e9831d59..93419608555 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -4,6 +4,7 @@ ABCEGHJKLMNPRSTVXY accessibilities agrc Alderaan +ALPN Andi apphost appsettings