From ab6474957f80a43e017edc5e16d228afadeddc6d Mon Sep 17 00:00:00 2001 From: Pascal Senn Date: Sat, 28 Mar 2026 14:10:37 +0100 Subject: [PATCH] Add diagnostics documentation for Mocha source generator --- website/src/docs/docs.json | 4 + website/src/docs/mocha/v1/diagnostics.md | 468 ++++++++++++++++++ .../src/docs/mocha/v1/handler-registration.md | 127 +---- 3 files changed, 475 insertions(+), 124 deletions(-) create mode 100644 website/src/docs/mocha/v1/diagnostics.md diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 8398bb0e6e6..8827168893b 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -2908,6 +2908,10 @@ "title": "Pipeline & Middleware" } ] + }, + { + "path": "diagnostics", + "title": "Diagnostics" } ] } diff --git a/website/src/docs/mocha/v1/diagnostics.md b/website/src/docs/mocha/v1/diagnostics.md new file mode 100644 index 00000000000..f49249288cc --- /dev/null +++ b/website/src/docs/mocha/v1/diagnostics.md @@ -0,0 +1,468 @@ +--- +title: "Diagnostics" +description: "Reference for all compile-time diagnostics emitted by the Mocha source generator, including causes, examples, and fixes for each warning and error." +--- + +# 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. + +# Quick reference + +| Code | Description | Severity | +| ----------------- | -------------------------------------------------------- | -------- | +| [MO0001](#mo0001) | Missing handler for message type | Warning | +| [MO0002](#mo0002) | Duplicate handler for message type | Error | +| [MO0003](#mo0003) | Handler is abstract | Warning | +| [MO0004](#mo0004) | Open generic message type cannot be dispatched | Info | +| [MO0005](#mo0005) | Handler implements multiple mediator handler interfaces | Error | +| [MO0011](#mo0011) | Duplicate handler for request type | Error | +| [MO0012](#mo0012) | Open generic messaging handler cannot be auto-registered | Info | +| [MO0013](#mo0013) | Messaging handler is abstract | Warning | +| [MO0014](#mo0014) | Saga must have a public parameterless constructor | Error | + +# Mediator diagnostics + +These diagnostics apply to the in-process [mediator](/docs/mocha/v1/mediator) — commands, queries, and notifications dispatched within a single process. + +## MO0001 + +**Missing handler for message type** + +| | | +| ------------ | ---------------------------------------------- | +| **Severity** | Warning | +| **Message** | `Message type '{0}' has no registered handler` | + +### Cause + +A command or query type is declared but no corresponding handler implementation exists. The mediator requires exactly one handler for each command and query type. This diagnostic does not apply to notifications, which can have zero handlers. + +### Example + +```csharp +using Mocha.Mediator; + +// Command with no handler — triggers MO0001 +public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; +``` + +### Fix + +Implement a handler for the message type. + +```csharp +using Mocha.Mediator; + +public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; + +public class PlaceOrderHandler : ICommandHandler +{ + public ValueTask HandleAsync( + PlaceOrder command, + CancellationToken cancellationToken) + { + // process the order + return ValueTask.CompletedTask; + } +} +``` + +## MO0002 + +**Duplicate handler for message type** + +| | | +| ------------ | ----------------------------------------------- | +| **Severity** | Error | +| **Message** | `Message type '{0}' has multiple handlers: {1}` | + +### 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. + +### Example + +```csharp +using Mocha.Mediator; + +public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; + +// Two handlers for the same command — triggers MO0002 +public class PlaceOrderHandler : ICommandHandler +{ + public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) + => ValueTask.CompletedTask; +} + +public class DuplicateOrderHandler : ICommandHandler +{ + public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +### Fix + +Remove all but one handler. If you need multiple side effects for the same action, consider publishing a notification from the single handler and reacting to it with separate notification handlers. + +```csharp +using Mocha.Mediator; + +public record PlaceOrder(Guid OrderId, decimal Total) : ICommand; + +public class PlaceOrderHandler : ICommandHandler +{ + public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +## MO0003 + +**Handler is abstract** + +| | | +| ------------ | ------------------------------------------------------ | +| **Severity** | Warning | +| **Message** | `Handler '{0}' is abstract and will not be registered` | + +### Cause + +A class implements a [handler](/docs/mocha/v1/handlers-and-consumers) interface (`ICommandHandler`, `IQueryHandler`, or `INotificationHandler`) but is declared `abstract`. The source generator skips abstract types because they cannot be instantiated. + +### Example + +```csharp +using Mocha.Mediator; + +public record GetOrderTotal(Guid OrderId) : IQuery; + +// Abstract handler — triggers MO0003 +public abstract class GetOrderTotalHandler : IQueryHandler +{ + public abstract ValueTask HandleAsync( + GetOrderTotal query, + CancellationToken cancellationToken); +} +``` + +### Fix + +Make the handler concrete. If you want shared base logic, move it to a base class that does not implement the handler interface, and have the concrete handler extend it. + +```csharp +using Mocha.Mediator; + +public record GetOrderTotal(Guid OrderId) : IQuery; + +public class GetOrderTotalHandler : IQueryHandler +{ + public ValueTask HandleAsync( + GetOrderTotal query, + CancellationToken cancellationToken) + { + return ValueTask.FromResult(99.99m); + } +} +``` + +## MO0004 + +**Open generic message type cannot be dispatched** + +| | | +| ------------ | --------------------------------------------------------------------------- | +| **Severity** | Info | +| **Message** | `Message type '{0}' is an open generic and cannot be dispatched at runtime` | + +### Cause + +A command or query type has unbound type parameters. The mediator dispatches concrete types at runtime and cannot resolve an open generic like `MyCommand`. + +### Example + +```csharp +using Mocha.Mediator; + +// Open generic command — triggers MO0004 +public record ProcessItem(T Item) : ICommand; +``` + +### Fix + +Use concrete message types instead. + +```csharp +using Mocha.Mediator; + +public record ProcessOrder(Guid OrderId) : ICommand; +public record ProcessPayment(decimal Amount) : ICommand; +``` + +## MO0005 + +**Handler implements multiple mediator handler interfaces** + +| | | +| ------------ | --------------------------------------------------------------------- | +| **Severity** | Error | +| **Message** | `Handler '{0}' must implement exactly one mediator handler interface` | + +### Cause + +A single class implements more than one of `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`. Each handler class must implement exactly one mediator handler interface so the generator can produce unambiguous registrations. + +### Example + +```csharp +using Mocha.Mediator; + +public record PlaceOrder(Guid OrderId) : ICommand; +public record GetOrder(Guid OrderId) : IQuery; + +// Implements both command and query handler — triggers MO0005 +public class OrderHandler + : ICommandHandler, + IQueryHandler +{ + public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) + => ValueTask.CompletedTask; + + public ValueTask HandleAsync(GetOrder query, CancellationToken ct) + => ValueTask.FromResult(new Order()); +} +``` + +### Fix + +Split into separate handler classes, one per interface. + +```csharp +using Mocha.Mediator; + +public record PlaceOrder(Guid OrderId) : ICommand; +public record GetOrder(Guid OrderId) : IQuery; + +public class PlaceOrderHandler : ICommandHandler +{ + public ValueTask HandleAsync(PlaceOrder command, CancellationToken ct) + => ValueTask.CompletedTask; +} + +public class GetOrderHandler : IQueryHandler +{ + public ValueTask HandleAsync(GetOrder query, CancellationToken ct) + => ValueTask.FromResult(new Order()); +} +``` + +# 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. + +## MO0011 + +**Duplicate handler for request type** + +| | | +| ------------ | ----------------------------------------------- | +| **Severity** | Error | +| **Message** | `Request type '{0}' has multiple handlers: {1}` | + +### 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. + +### Example + +```csharp +using Mocha; + +public record ProcessPayment(decimal Amount); + +// Two handlers for the same request type — triggers MO0011 +public class PaymentHandlerA : IEventRequestHandler +{ + public ValueTask HandleAsync( + ProcessPayment request, + CancellationToken ct) + => ValueTask.CompletedTask; +} + +public class PaymentHandlerB : IEventRequestHandler +{ + public ValueTask HandleAsync( + ProcessPayment request, + CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +### Fix + +Keep one handler per request type. + +```csharp +using Mocha; + +public record ProcessPayment(decimal Amount); + +public class ProcessPaymentHandler : IEventRequestHandler +{ + public ValueTask HandleAsync( + ProcessPayment request, + CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +## MO0012 + +**Open generic messaging handler cannot be auto-registered** + +| | | +| ------------ | ---------------------------------------------------------------- | +| **Severity** | Info | +| **Message** | `Handler '{0}' is an open generic and cannot be auto-registered` | + +### Cause + +A messaging handler (`IEventHandler`, `IEventRequestHandler`, `IBatchEventHandler`, or `IConsumer`) has unbound type parameters. The source generator cannot produce registration code for open generic types. + +### Example + +```csharp +using Mocha; + +// Open generic handler — triggers MO0012 +public class GenericEventHandler : IEventHandler +{ + public ValueTask HandleAsync( + T message, + CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +### Fix + +Make the handler concrete. If you need to handle multiple event types with shared logic, create a concrete handler for each type and extract the shared logic into a base class or shared service. + +If you need to register an open generic handler, register it manually through DI instead of relying on auto-registration. + +```csharp +using Mocha; + +public record OrderPlaced(Guid OrderId); + +public class OrderPlacedHandler : IEventHandler +{ + public ValueTask HandleAsync( + OrderPlaced message, + CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +## MO0013 + +**Messaging handler is abstract** + +| | | +| ------------ | ------------------------------------------------------ | +| **Severity** | Warning | +| **Message** | `Handler '{0}' is abstract and will not be registered` | + +### Cause + +A class implements a messaging [handler](/docs/mocha/v1/handlers-and-consumers) interface but is declared `abstract`. The source generator skips abstract types because they cannot be instantiated. + +### Example + +```csharp +using Mocha; + +public record OrderPlaced(Guid OrderId); + +// Abstract handler — triggers MO0013 +public abstract class OrderEventHandler : IEventHandler +{ + public abstract ValueTask HandleAsync( + OrderPlaced message, + CancellationToken ct); +} +``` + +### Fix + +Make the handler concrete. If you need shared base logic, move it to a base class that does not implement the handler interface. + +```csharp +using Mocha; + +public record OrderPlaced(Guid OrderId); + +public class OrderPlacedHandler : IEventHandler +{ + public ValueTask HandleAsync( + OrderPlaced message, + CancellationToken ct) + => ValueTask.CompletedTask; +} +``` + +## MO0014 + +**Saga must have a public parameterless constructor** + +| | | +| ------------ | --------------------------------------------------------- | +| **Severity** | Error | +| **Message** | `Saga '{0}' must have a public parameterless constructor` | + +### Cause + +A [`Saga`](/docs/mocha/v1/sagas) subclass does not have a public parameterless constructor. The saga infrastructure requires this constructor to instantiate the saga type. This is enforced by the `new()` constraint on the `AddSaga` registration method. + +### Example + +```csharp +using Mocha.Sagas; + +public class RefundSagaState : SagaStateBase +{ + public Guid OrderId { get; set; } +} + +// Constructor requires a parameter — triggers MO0014 +public class RefundSaga : Saga +{ + private readonly ILogger _logger; + + public RefundSaga(ILogger logger) + { + _logger = logger; + } +} +``` + +### Fix + +Add a public parameterless constructor. Sagas are configured through their state machine definition, not through constructor injection. If you need dependencies, access them through the saga's built-in service resolution. + +```csharp +using Mocha.Sagas; + +public class RefundSagaState : SagaStateBase +{ + public Guid OrderId { get; set; } +} + +public class RefundSaga : Saga +{ + public RefundSaga() + { + } +} +``` diff --git a/website/src/docs/mocha/v1/handler-registration.md b/website/src/docs/mocha/v1/handler-registration.md index 81c6d365e0a..08183ffde05 100644 --- a/website/src/docs/mocha/v1/handler-registration.md +++ b/website/src/docs/mocha/v1/handler-registration.md @@ -83,127 +83,6 @@ You can mix source-generated and manual registration freely. If both the source > **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. -# Analyzer diagnostics - -The source generator reports compile-time diagnostics when it finds issues with your handlers. These appear as warnings or errors in your IDE and build output. - -| Code | Severity | Message | Cause | -| ---------- | -------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| **MO0010** | Warning | Request type '{0}' has no registered handler | An `IEventRequest` type exists but no handler implements `IEventRequestHandler` or `IEventRequestHandler` for it | -| **MO0011** | Error | Request type '{0}' has multiple handlers: {1} | Two or more handlers implement `IEventRequestHandler` for the same request type. Request handlers must be unique per request type | -| **MO0012** | Info | Handler '{0}' is an open generic and cannot be auto-registered | A handler like `GenericHandler : IEventHandler` has unbound type parameters. Register closed generic versions manually | -| **MO0013** | Warning | Handler '{0}' is abstract and will not be registered | An abstract class implements a handler interface. Only concrete classes are registered. This is expected for base handler classes | -| **MO0014** | Error | Saga '{0}' must have a public parameterless constructor | A `Saga` subclass is missing a public parameterless constructor, which is required for saga state management | - -## MO0010: Missing request handler - -A request type implements `IEventRequest` but no handler in the assembly handles it: - -```csharp -// This produces MO0010 -public record GetOrderStatusRequest(Guid OrderId) : IEventRequest; - -// No class implements IEventRequestHandler -``` - -**Fix:** Add a handler for the request type: - -```csharp -public class GetOrderStatusHandler - : IEventRequestHandler -{ - public ValueTask HandleAsync( - GetOrderStatusRequest request, - CancellationToken ct) - => new("shipped"); -} -``` - -## MO0011: Duplicate request handler - -Request types can have only one handler. If two handlers register for the same request type, the build fails: - -```csharp -// This produces MO0011 -public class GetOrderHandlerA : IEventRequestHandler -{ - public ValueTask HandleAsync( - GetOrderRequest request, - CancellationToken ct) - => new("A"); -} - -public class GetOrderHandlerB : IEventRequestHandler -{ - public ValueTask HandleAsync( - GetOrderRequest request, - CancellationToken ct) - => new("B"); -} -``` - -**Fix:** Remove one of the handlers, or change one to handle a different request type. - -## MO0012: Open generic handler - -Open generics cannot be auto-registered because the source generator needs concrete type arguments: - -```csharp -// This produces MO0012 -public class GenericHandler : IEventHandler -{ - public ValueTask HandleAsync(T message, CancellationToken ct) => default; -} -``` - -**Fix:** Create closed generic implementations for each message type: - -```csharp -public class OrderPlacedHandler : GenericHandler { } -public class PaymentReceivedHandler : GenericHandler { } -``` - -## MO0013: Abstract handler - -Abstract handlers are not registered - they cannot be instantiated by DI. This diagnostic is informational; it confirms the generator is intentionally skipping your base class. - -```csharp -// This produces MO0013 - expected for base classes -public abstract class BaseOrderHandler : IEventHandler -{ - public abstract ValueTask HandleAsync(OrderPlaced message, CancellationToken ct); -} - -// Concrete subclass is registered normally -public class OrderPlacedHandler : BaseOrderHandler -{ - public override ValueTask HandleAsync(OrderPlaced message, CancellationToken ct) => default; -} -``` - -## MO0014: Saga without parameterless constructor - -Sagas require a public parameterless constructor for state management: - -```csharp -// This produces MO0014 -public class OrderSaga : Saga -{ - public OrderSaga(string name) { } // no parameterless constructor - - protected override void Configure(ISagaDescriptor descriptor) { } -} -``` - -**Fix:** Add a public parameterless constructor, or remove the constructor with parameters and let the compiler generate the default: - -```csharp -public class OrderSaga : Saga -{ - protected override void Configure(ISagaDescriptor descriptor) { } -} -``` - # Troubleshooting ## The source-generated method does not appear @@ -212,15 +91,15 @@ If IntelliSense does not show `Add{ModuleName}()`: - Confirm the `Mocha.Analyzers` package is referenced with `OutputItemType="Analyzer"` in your `.csproj` - Rebuild the project - source generators run during compilation -- Check the build output for analyzer warnings prefixed with `MO` +- Check the build output for [analyzer diagnostics](/docs/mocha/v1/diagnostics) prefixed with `MO` - Verify you have at least one concrete handler class in the assembly ## Handler is not being called If the source-generated method is available but a specific handler does not run: -- Check for **MO0013** (abstract handler) - only concrete classes are registered -- Check for **MO0012** (open generic) - close the generic type +- Check for [**MO0013**](/docs/mocha/v1/diagnostics#mo0013) (abstract handler) - only concrete classes are registered +- Check for [**MO0012**](/docs/mocha/v1/diagnostics#mo0012) (open generic) - close the generic type - Verify the handler implements the correct interface for the messaging pattern you are using - Ensure the handler is in the same project that references `Mocha.Analyzers`