src/Mocha/src/Mocha.Analyzers/README.md
Incremental Roslyn source generator that discovers messaging handlers, sagas, and call-sites from a compilation and emits a dependency injection registration method (Add{Module}()) for the message bus.
A module is declared via an assembly-level attribute:
[assembly: MessagingModule("OrderService")]
With AOT support:
[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
Add{ModuleName}).[MessagingModule] per assembly is supported.The generator runs two parallel incremental pipelines:
Syntax predicate filters classes/records with Mocha base types in their base list, then inspectors run in priority order:
[assembly: MessagingModule(...)] attributesSaga<TState> subclassesSyntax predicate filters invocation expressions, then:
IMessageBus, ISender, IPublisher[MessagingModuleInfo]Both pipelines feed into the Execute method which validates, augments, and generates code.
Concrete (non-abstract, non-generic) classes or records implementing messaging interfaces are discovered. The inspector checks interfaces in a priority cascade - first match wins:
| Priority | Interface | Kind | Response |
|---|---|---|---|
| 1 | IBatchEventHandler<T> | Batch | No |
| 2 | IConsumer<T> | Consumer | No |
| 3 | IEventRequestHandler<T, TResponse> | RequestResponse | Yes |
| 4 | IEventRequestHandler<T> | Send | No |
| 5 | IEventHandler<T> | Event | No |
For each discovered handler, the full type hierarchy of the message type is captured (all base types excluding object, plus all interfaces). This hierarchy is used for enclosed type computation in AOT mode.
Classes (not records) that inherit from Saga<TState> are discovered. The inspector:
Saga<TState> and extracts TState.Invocations on IMessageBus, ISender, and IPublisher are inspected to discover message types used at call sites. These are used for validation only (no code generation).
| Method | CallSiteKind | Type Extraction |
|---|---|---|
PublishAsync<T> | Publish | Type argument |
SendAsync<T> | Send | Type argument |
SchedulePublishAsync<T> | SchedulePublish | Type argument |
ScheduleSendAsync<T> | ScheduleSend | Type argument |
RequestAsync<TResponse> | Request | First argument type + type argument for response |
RequestAsync (non-generic) | Request | First argument type (fallback) |
| Method | CallSiteKind | Type Extraction |
|---|---|---|
SendAsync | MediatorSend | First argument type |
QueryAsync | MediatorQuery | First argument type |
| Method | CallSiteKind | Type Extraction |
|---|---|---|
PublishAsync<T> | MediatorPublish | Type argument |
Mediator call sites are excluded from JSON validation because mediator dispatch is in-process and does not require serialization.
When code calls a method annotated with [MessagingModuleInfo] (e.g., builder.AddOrders()), the inspector reads the MessageTypes array from the attribute. These types are treated as "already registered" and:
AddMessageConfiguration)The generator emits a single extension method on IMessageBusHostBuilder:
namespace Microsoft.Extensions.DependencyInjection
{
public static class {Module}MessageBusBuilderExtensions
{
[MessagingModuleInfo(MessageTypes = new Type[] { ... })]
public static IMessageBusHostBuilder Add{Module}(
this IMessageBusHostBuilder builder)
{
// registrations
return builder;
}
}
}
Registrations are emitted in this order:
ModifyOptions(builder, o => o.IsAotCompatible = true)AddJsonTypeInfoResolver(builder, {JsonContext}.Default)AddMessageConfiguration per typeAddSagaConfiguration<TSaga> with state serializerAddSaga<TSaga>Each handler emits AddHandlerConfiguration<THandler> with a factory:
| Kind | Factory |
|---|---|
| Event | ConsumerFactory.Subscribe<THandler, TMessage>() |
| Send | ConsumerFactory.Send<THandler, TMessage>() |
| RequestResponse | ConsumerFactory.Request<THandler, TMessage, TResponse>() |
| Consumer | ConsumerFactory.Consume<THandler, TMessage>() |
| Batch | ConsumerFactory.Batch<THandler, TMessage>() |
[MessagingModuleInfo] Attribute PopulationThe MessageTypes array on the generated method contains only types that receive AddMessageConfiguration calls in the method body. This means:
JsonSerializerContext (not from referenced assemblies)This ensures importing modules know exactly which types have serializer registrations from this module.
AOT mode has two independent aspects controlled by different settings:
JsonContext on [MessagingModule] - controls code generation: serializer registrations and resolver registration are emitted when a JsonContext is specified.PublishAot MSBuild property - controls strict mode and validation strictness: when true, the generated module enables AOT-compatible runtime mode and diagnostics MO0015/MO0016/MO0018 fire.Validation diagnostics fire when PublishAot is true. Serializer code generation requires JsonContext.
Serializer registrations are emitted - AddMessageConfiguration with pre-built JsonMessageSerializer for each message type in the local JsonContext.
JsonTypeInfoResolver is registered - the specified JsonSerializerContext is added as a resolver.
AOT strict mode is not enabled by JsonContext alone - IsAotCompatible = true is emitted only when PublishAot is true.
Validation diagnostics do not fire by JsonContext alone - MO0015, MO0016, and MO0018 are checked only when PublishAot is true.
A type gets an AddMessageConfiguration call if all of these are true:
[JsonSerializable(typeof(T))] on the local JsonSerializerContextTypes declared in the JsonSerializerContext that have no corresponding handler or saga in the current assembly still receive AddMessageConfiguration registrations. These are types the module needs to serialize but doesn't consume.
For each message type registration, the generator computes an "enclosed types" array from the type hierarchy. If multiple registered types share a hierarchy (e.g., OrderUpdated : Order), enclosed types are sorted by specificity - most specific types first. This supports polymorphic serialization.
Fires when: Multiple handlers exist for the same message type with Send or RequestResponse kind.
Example: Two handlers both implement IEventRequestHandler<CheckInventoryRequest, CheckInventoryResponse>.
Fires when: A handler class has unbound type parameters (e.g., class MyHandler<T> : IEventHandler<T>).
Reason: The generator cannot register open generic handlers - concrete types are required.
Fires when: An abstract class implements a messaging interface.
Reason: Abstract classes cannot be instantiated and thus cannot be registered as handlers.
Fires when: A Saga<TState> subclass does not have a public parameterless constructor.
Reason: The saga runtime requires new() to instantiate saga instances.
Fires when (AOT mode): The module has handlers or sagas with message types not fully covered by imported modules, but no JsonContext is specified on [MessagingModule].
Fix: Add JsonContext = typeof(MyJsonContext) to the attribute.
Fires when (AOT mode): A handler message type, response type, or saga state type is not declared as [JsonSerializable] on the local JsonSerializerContext and not covered by imported modules.
Fix: Add [JsonSerializable(typeof(MissingType))] to the JsonContext class.
Fires when (AOT mode): A message type used in a dispatch call (PublishAsync, SendAsync, etc.) is not found in the local JsonSerializerContext or imported module types.
Scope: Only messaging dispatch calls - mediator dispatch (ISender.SendAsync, ISender.QueryAsync, IPublisher.PublishAsync) is excluded because it's in-process.
Note: MO0017 is reserved and not currently used.
| Diagnostic | Condition |
|---|---|
| MO0011 | Always (not AOT-gated) |
| MO0012 | Always (not AOT-gated) |
| MO0013 | Always (not AOT-gated) |
| MO0014 | Always (not AOT-gated) |
| MO0015 | PublishAot == true |
| MO0016 | PublishAot == true |
| MO0018 | PublishAot == true |
Handlers or sagas that carry a diagnostic (e.g., MO0012, MO0013, MO0014) are excluded from code generation - no AddHandlerConfiguration or AddSagaConfiguration is emitted for them. Only entries with zero diagnostics flow to the generator.
The module system enables multiple assemblies to register their handlers independently while avoiding duplicate serializer registrations.
[assembly: MessagingModule("Orders", JsonContext = typeof(OrdersJsonContext))]AddOrders() with [MessagingModuleInfo(MessageTypes = new[] { typeof(OrderCreated), ... })]builder.AddOrders() in its codeImportedModuleTypeInspector reads the [MessagingModuleInfo] attribute and extracts the type names[MessagingModuleInfo][JsonSerializable] neededThe [MessagingModuleInfo] attribute only advertises types for which the module emits AddMessageConfiguration calls. Types that are handled but don't have local serializer support (not in the local JsonContext) are not included in the attribute. This prevents downstream modules from incorrectly assuming serialization is covered.