diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/AotExample.Contracts.csproj b/src/Mocha/examples/AotExample/AotExample.Contracts/AotExample.Contracts.csproj
new file mode 100644
index 00000000000..fb76bbd8c8e
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/AotExample.Contracts.csproj
@@ -0,0 +1,13 @@
+
+
+ AotExample.Contracts
+
+
+
+
+
+
diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/AotExampleJsonContext.cs b/src/Mocha/examples/AotExample/AotExample.Contracts/AotExampleJsonContext.cs
new file mode 100644
index 00000000000..2bf9141cfab
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/AotExampleJsonContext.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using AotExample.Contracts.Events;
+using AotExample.Contracts.Requests;
+
+namespace AotExample.Contracts;
+
+[JsonSerializable(typeof(OrderPlacedEvent))]
+[JsonSerializable(typeof(OrderShippedEvent))]
+[JsonSerializable(typeof(CheckInventoryRequest))]
+[JsonSerializable(typeof(CheckInventoryResponse))]
+public partial class AotExampleJsonContext : JsonSerializerContext;
diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/AssemblyInfo.cs b/src/Mocha/examples/AotExample/AotExample.Contracts/AssemblyInfo.cs
new file mode 100644
index 00000000000..eee01dc673c
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using AotExample.Contracts;
+using Mocha;
+
+[assembly: MessagingModule(
+ "AotExampleContracts",
+ JsonContext = typeof(AotExampleJsonContext))]
diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/Events/OrderPlacedEvent.cs b/src/Mocha/examples/AotExample/AotExample.Contracts/Events/OrderPlacedEvent.cs
new file mode 100644
index 00000000000..7c10e381f99
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/Events/OrderPlacedEvent.cs
@@ -0,0 +1,14 @@
+using Mocha.Sagas;
+
+namespace AotExample.Contracts.Events;
+
+public sealed class OrderPlacedEvent : ICorrelatable
+{
+ public required string OrderId { get; init; }
+
+ public required string ProductName { get; init; }
+
+ public required int Quantity { get; init; }
+
+ public Guid? CorrelationId { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/Events/OrderShippedEvent.cs b/src/Mocha/examples/AotExample/AotExample.Contracts/Events/OrderShippedEvent.cs
new file mode 100644
index 00000000000..750e3b916c0
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/Events/OrderShippedEvent.cs
@@ -0,0 +1,12 @@
+using Mocha.Sagas;
+
+namespace AotExample.Contracts.Events;
+
+public sealed class OrderShippedEvent : ICorrelatable
+{
+ public required string OrderId { get; init; }
+
+ public required string TrackingNumber { get; init; }
+
+ public Guid? CorrelationId { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/Requests/CheckInventoryRequest.cs b/src/Mocha/examples/AotExample/AotExample.Contracts/Requests/CheckInventoryRequest.cs
new file mode 100644
index 00000000000..d3efc770127
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/Requests/CheckInventoryRequest.cs
@@ -0,0 +1,9 @@
+using Mocha;
+
+namespace AotExample.Contracts.Requests;
+
+public sealed class CheckInventoryRequest : IEventRequest
+{
+ public required string ProductName { get; init; }
+ public required int Quantity { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.Contracts/Requests/CheckInventoryResponse.cs b/src/Mocha/examples/AotExample/AotExample.Contracts/Requests/CheckInventoryResponse.cs
new file mode 100644
index 00000000000..b60d5bc6de3
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.Contracts/Requests/CheckInventoryResponse.cs
@@ -0,0 +1,7 @@
+namespace AotExample.Contracts.Requests;
+
+public sealed class CheckInventoryResponse
+{
+ public required bool IsAvailable { get; init; }
+ public required int QuantityOnHand { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.FulfillmentService/AotExample.FulfillmentService.csproj b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/AotExample.FulfillmentService.csproj
new file mode 100644
index 00000000000..ad309fa182d
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/AotExample.FulfillmentService.csproj
@@ -0,0 +1,17 @@
+
+
+ AotExample.FulfillmentService
+ true
+ true
+
+
+
+
+
+
+
+
diff --git a/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Handlers/OrderPlacedHandler.cs b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Handlers/OrderPlacedHandler.cs
new file mode 100644
index 00000000000..4731561dba4
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Handlers/OrderPlacedHandler.cs
@@ -0,0 +1,50 @@
+using AotExample.Contracts.Events;
+using AotExample.Contracts.Requests;
+using Mocha;
+
+namespace AotExample.FulfillmentService.Handlers;
+
+public sealed class OrderPlacedHandler(IMessageBus messageBus, ILogger logger)
+ : IEventHandler
+{
+ public async ValueTask HandleAsync(OrderPlacedEvent message, CancellationToken cancellationToken)
+ {
+ logger.LogOrderReceived(message.OrderId, message.Quantity, message.ProductName);
+
+ // Check inventory with OrderService via bus request/response
+ var inventory = await messageBus.RequestAsync(
+ new CheckInventoryRequest { ProductName = message.ProductName, Quantity = message.Quantity },
+ cancellationToken);
+
+ if (!inventory.IsAvailable)
+ {
+ logger.LogCannotFulfillOrder(message.OrderId, inventory.QuantityOnHand, message.ProductName);
+ return;
+ }
+
+ var trackingNumber = $"TRK-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
+
+ await messageBus.PublishAsync(
+ new OrderShippedEvent
+ {
+ OrderId = message.OrderId,
+ TrackingNumber = trackingNumber,
+ CorrelationId = message.CorrelationId
+ },
+ cancellationToken);
+
+ logger.LogOrderFulfilled(message.OrderId, trackingNumber);
+ }
+}
+
+internal static partial class Logs
+{
+ [LoggerMessage(Level = LogLevel.Information, Message = "Received order {OrderId}: {Quantity}x {ProductName}")]
+ public static partial void LogOrderReceived(this ILogger logger, string orderId, int quantity, string productName);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Cannot fulfill order {OrderId}: only {QuantityOnHand} of {ProductName} on hand")]
+ public static partial void LogCannotFulfillOrder(this ILogger logger, string orderId, int quantityOnHand, string productName);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} fulfilled — tracking {TrackingNumber}")]
+ public static partial void LogOrderFulfilled(this ILogger logger, string orderId, string trackingNumber);
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Program.cs b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Program.cs
new file mode 100644
index 00000000000..bfade8c8f1e
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Program.cs
@@ -0,0 +1,20 @@
+using Mocha;
+using Mocha.Transport.RabbitMQ;
+using RabbitMQ.Client;
+
+[assembly: MessagingModule("FulfillmentService")]
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.WebHost.UseUrls("http://localhost:52310");
+
+builder.Services.AddSingleton(
+ new ConnectionFactory { HostName = "localhost", Port = 5673 });
+
+builder.Services.AddMessageBus().AddAotExampleContracts().AddFulfillmentService().AddRabbitMQ();
+
+var app = builder.Build();
+
+app.MapGet("/", () => "Fulfillment Service (AOT Example)");
+
+app.Run();
diff --git a/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Properties/launchSettings.json b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Properties/launchSettings.json
new file mode 100644
index 00000000000..2c61fbf750f
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.FulfillmentService/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "AotExample.FulfillmentService": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5247",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/AotExample.OrderService.csproj b/src/Mocha/examples/AotExample/AotExample.OrderService/AotExample.OrderService.csproj
new file mode 100644
index 00000000000..9843f1ac432
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/AotExample.OrderService.csproj
@@ -0,0 +1,19 @@
+
+
+ AotExample.OrderService
+ true
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Commands/PlaceOrderCommand.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Commands/PlaceOrderCommand.cs
new file mode 100644
index 00000000000..01ce0ec7180
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Commands/PlaceOrderCommand.cs
@@ -0,0 +1,14 @@
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Commands;
+
+public sealed class PlaceOrderCommand : ICommand
+{
+ public required string ProductName { get; init; }
+ public required int Quantity { get; init; }
+}
+
+public sealed class PlaceOrderResult
+{
+ public required string OrderId { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/CheckInventoryRequestHandler.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/CheckInventoryRequestHandler.cs
new file mode 100644
index 00000000000..a956dcd7bdd
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/CheckInventoryRequestHandler.cs
@@ -0,0 +1,38 @@
+using AotExample.Contracts.Requests;
+using Mocha;
+
+namespace AotExample.OrderService.Handlers;
+
+public sealed class CheckInventoryRequestHandler(ILogger logger)
+ : IEventRequestHandler
+{
+ public ValueTask HandleAsync(
+ CheckInventoryRequest request,
+ CancellationToken cancellationToken)
+ {
+ var quantityOnHand = Random.Shared.Next(0, 20);
+ var isAvailable = quantityOnHand >= request.Quantity;
+
+ logger.LogInventoryCheck(
+ request.ProductName,
+ quantityOnHand,
+ request.Quantity,
+ isAvailable ? "available" : "insufficient");
+
+ return new ValueTask(
+ new CheckInventoryResponse { IsAvailable = isAvailable, QuantityOnHand = quantityOnHand });
+ }
+}
+
+internal static partial class Logs
+{
+ [LoggerMessage(
+ Level = LogLevel.Information,
+ Message = "Inventory check for {ProductName}: {QuantityOnHand} on hand, requested {Quantity} — {Result}")]
+ public static partial void LogInventoryCheck(
+ this ILogger logger,
+ string productName,
+ int quantityOnHand,
+ int quantity,
+ string result);
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/GetOrderStatusQueryHandler.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/GetOrderStatusQueryHandler.cs
new file mode 100644
index 00000000000..16410feceb4
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/GetOrderStatusQueryHandler.cs
@@ -0,0 +1,20 @@
+using AotExample.OrderService.Queries;
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Handlers;
+
+public sealed class GetOrderStatusQueryHandler
+ : IQueryHandler
+{
+ public ValueTask HandleAsync(
+ GetOrderStatusQuery query,
+ CancellationToken cancellationToken)
+ {
+ return new ValueTask(
+ new GetOrderStatusResponse
+ {
+ OrderId = query.OrderId,
+ Status = "Processing"
+ });
+ }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/OrderShippedHandler.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/OrderShippedHandler.cs
new file mode 100644
index 00000000000..0dad1898e97
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/OrderShippedHandler.cs
@@ -0,0 +1,25 @@
+using AotExample.Contracts.Events;
+using AotExample.OrderService.Notifications;
+using Mocha;
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Handlers;
+
+public sealed class OrderShippedHandler(IPublisher publisher, ILogger logger)
+ : IEventHandler
+{
+ public async ValueTask HandleAsync(OrderShippedEvent message, CancellationToken cancellationToken)
+ {
+ logger.LogOrderShipped(message.OrderId, message.TrackingNumber);
+
+ await publisher.PublishAsync(
+ new OrderStatusChangedNotification { OrderId = message.OrderId, Status = "Shipped" },
+ cancellationToken);
+ }
+}
+
+internal static partial class Logs
+{
+ [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} shipped with tracking {TrackingNumber}")]
+ public static partial void LogOrderShipped(this ILogger logger, string orderId, string trackingNumber);
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/OrderStatusChangedHandler.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/OrderStatusChangedHandler.cs
new file mode 100644
index 00000000000..f85aea563b3
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/OrderStatusChangedHandler.cs
@@ -0,0 +1,21 @@
+using AotExample.OrderService.Notifications;
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Handlers;
+
+public sealed class OrderStatusChangedHandler(ILogger logger)
+ : INotificationHandler
+{
+ public ValueTask HandleAsync(OrderStatusChangedNotification notification, CancellationToken cancellationToken)
+ {
+ logger.LogOrderStatusChanged(notification.OrderId, notification.Status);
+
+ return ValueTask.CompletedTask;
+ }
+}
+
+internal static partial class Logs
+{
+ [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} status changed to {Status}")]
+ public static partial void LogOrderStatusChanged(this ILogger logger, string orderId, string status);
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/PlaceOrderCommandHandler.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/PlaceOrderCommandHandler.cs
new file mode 100644
index 00000000000..7a5a13926a0
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Handlers/PlaceOrderCommandHandler.cs
@@ -0,0 +1,23 @@
+using AotExample.OrderService.Commands;
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Handlers;
+
+public sealed class PlaceOrderCommandHandler(ILogger logger)
+ : ICommandHandler
+{
+ public ValueTask HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken)
+ {
+ var orderId = Guid.NewGuid().ToString("N")[..8];
+
+ logger.LogOrderPlaced(orderId, command.Quantity, command.ProductName);
+
+ return new ValueTask(new PlaceOrderResult { OrderId = orderId });
+ }
+}
+
+internal static partial class Logs
+{
+ [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} placed: {Quantity}x {ProductName}")]
+ public static partial void LogOrderPlaced(this ILogger logger, string orderId, int quantity, string productName);
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Notifications/OrderStatusChangedNotification.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Notifications/OrderStatusChangedNotification.cs
new file mode 100644
index 00000000000..23ca8b6f6b6
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Notifications/OrderStatusChangedNotification.cs
@@ -0,0 +1,10 @@
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Notifications;
+
+public sealed class OrderStatusChangedNotification : INotification
+{
+ public required string OrderId { get; init; }
+
+ public required string Status { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/OrderServiceJsonContext.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/OrderServiceJsonContext.cs
new file mode 100644
index 00000000000..ada8c441f88
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/OrderServiceJsonContext.cs
@@ -0,0 +1,7 @@
+using System.Text.Json.Serialization;
+using AotExample.OrderService.Sagas;
+
+namespace AotExample.OrderService;
+
+[JsonSerializable(typeof(OrderSagaState))]
+public partial class OrderServiceJsonContext : JsonSerializerContext;
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/OrderSimulatorWorker.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/OrderSimulatorWorker.cs
new file mode 100644
index 00000000000..795168fba1e
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/OrderSimulatorWorker.cs
@@ -0,0 +1,102 @@
+using AotExample.Contracts.Events;
+using AotExample.OrderService.Commands;
+using AotExample.OrderService.Queries;
+using Mocha;
+using Mocha.Mediator;
+
+namespace AotExample.OrderService;
+
+public sealed class OrderSimulatorWorker(
+ IServiceScopeFactory scopeFactory,
+ ILogger logger
+) : BackgroundService
+{
+ private static readonly string[] s_products =
+ [
+ "Mechanical Keyboard",
+ "Wireless Mouse",
+ "USB-C Hub",
+ "Monitor Stand",
+ "Webcam HD",
+ "Noise-Cancelling Headphones",
+ ];
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await Task.Delay(3000, stoppingToken);
+
+ logger.LogSimulatorStarted();
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await using var scope = scopeFactory.CreateAsyncScope();
+ var sender = scope.ServiceProvider.GetRequiredService();
+ var messageBus = scope.ServiceProvider.GetRequiredService();
+
+ var product = s_products[Random.Shared.Next(s_products.Length)];
+ var quantity = Random.Shared.Next(1, 6);
+
+ // 1. Place order via mediator command
+ var result = await sender.SendAsync(
+ new PlaceOrderCommand { ProductName = product, Quantity = quantity },
+ stoppingToken
+ );
+
+ // 2. Query order status via mediator query
+ var status = await sender.QueryAsync(
+ new GetOrderStatusQuery { OrderId = result.OrderId },
+ stoppingToken
+ );
+
+ logger.LogOrderStatus(result.OrderId, status.Status);
+
+ var correlationId = Guid.NewGuid();
+
+ // 3. Publish to bus — FulfillmentService will pick this up, saga will track it
+
+ await messageBus.PublishAsync(
+ new OrderShippedEvent
+ {
+ OrderId = result.OrderId,
+ TrackingNumber = $"TRACK-{Random.Shared.Next(1000, 9999)}",
+ },
+ stoppingToken
+ );
+ await messageBus.PublishAsync(
+ new OrderPlacedEvent
+ {
+ OrderId = result.OrderId,
+ ProductName = product,
+ Quantity = quantity,
+ CorrelationId = correlationId,
+ },
+ stoppingToken
+ );
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ logger.LogSimulationError(ex);
+ }
+
+ await Task.Delay(5000, stoppingToken);
+ }
+ }
+}
+
+internal static partial class Logs
+{
+ [LoggerMessage(Level = LogLevel.Information, Message = "Order simulator started")]
+ public static partial void LogSimulatorStarted(this ILogger logger);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} status: {Status}")]
+ public static partial void LogOrderStatus(this ILogger logger, string orderId, string status);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Error in order simulation")]
+ public static partial void LogSimulationError(this ILogger logger, Exception ex);
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Program.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Program.cs
new file mode 100644
index 00000000000..8303f3b5419
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Program.cs
@@ -0,0 +1,28 @@
+using AotExample.OrderService;
+using Mocha;
+using Mocha.Mediator;
+using Mocha.Transport.RabbitMQ;
+using RabbitMQ.Client;
+
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+[assembly: MediatorModule("OrderService")]
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.WebHost.UseUrls("http://localhost:52300");
+
+builder.Services.AddSingleton(
+ new ConnectionFactory { HostName = "localhost", Port = 5673 }
+);
+
+builder.Services.AddHostedService();
+
+builder.Services.AddMessageBus().AddAotExampleContracts().AddOrderService().AddRabbitMQ();
+
+builder.Services.AddMediator().AddOrderService();
+
+var app = builder.Build();
+
+app.MapGet("/", () => "Order Service (AOT Example)");
+
+app.Run();
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Properties/launchSettings.json b/src/Mocha/examples/AotExample/AotExample.OrderService/Properties/launchSettings.json
new file mode 100644
index 00000000000..66553d0b7a1
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5098",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Queries/GetOrderStatusQuery.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Queries/GetOrderStatusQuery.cs
new file mode 100644
index 00000000000..f4acae2726e
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Queries/GetOrderStatusQuery.cs
@@ -0,0 +1,8 @@
+using Mocha.Mediator;
+
+namespace AotExample.OrderService.Queries;
+
+public sealed class GetOrderStatusQuery : IQuery
+{
+ public required string OrderId { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Queries/GetOrderStatusResponse.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Queries/GetOrderStatusResponse.cs
new file mode 100644
index 00000000000..7e2340cbe4e
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Queries/GetOrderStatusResponse.cs
@@ -0,0 +1,8 @@
+namespace AotExample.OrderService.Queries;
+
+public sealed class GetOrderStatusResponse
+{
+ public required string OrderId { get; init; }
+
+ public required string Status { get; init; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Sagas/OrderSaga.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Sagas/OrderSaga.cs
new file mode 100644
index 00000000000..8eee1e78de2
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Sagas/OrderSaga.cs
@@ -0,0 +1,35 @@
+using AotExample.Contracts.Events;
+using Mocha.Sagas;
+
+namespace AotExample.OrderService.Sagas;
+
+public sealed class OrderSaga : Saga
+{
+ private const string AwaitingShipment = nameof(AwaitingShipment);
+ private const string Shipped = nameof(Shipped);
+
+ protected override void Configure(ISagaDescriptor descriptor)
+ {
+ descriptor.Timeout(TimeSpan.FromSeconds(30));
+
+ descriptor
+ .Initially()
+ .OnEvent()
+ .StateFactory(e => new OrderSagaState
+ {
+ OrderId = e.OrderId,
+ ProductName = e.ProductName,
+ Quantity = e.Quantity
+ })
+ .TransitionTo(AwaitingShipment);
+
+ descriptor
+ .During(AwaitingShipment)
+ .OnEvent()
+ .Then((state, e) => state.TrackingNumber = e.TrackingNumber)
+ .TransitionTo(Shipped);
+
+ descriptor
+ .Finally(Shipped);
+ }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.OrderService/Sagas/OrderSagaState.cs b/src/Mocha/examples/AotExample/AotExample.OrderService/Sagas/OrderSagaState.cs
new file mode 100644
index 00000000000..3664116c69f
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/Sagas/OrderSagaState.cs
@@ -0,0 +1,14 @@
+using Mocha.Sagas;
+
+namespace AotExample.OrderService.Sagas;
+
+public class OrderSagaState : SagaStateBase
+{
+ public string OrderId { get; set; } = string.Empty;
+
+ public string ProductName { get; set; } = string.Empty;
+
+ public int Quantity { get; set; }
+
+ public string? TrackingNumber { get; set; }
+}
diff --git a/src/Mocha/examples/AotExample/AotExample.slnx b/src/Mocha/examples/AotExample/AotExample.slnx
new file mode 100644
index 00000000000..e96d4733e5c
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mocha/examples/AotExample/Directory.Build.props b/src/Mocha/examples/AotExample/Directory.Build.props
new file mode 100644
index 00000000000..ac7170928de
--- /dev/null
+++ b/src/Mocha/examples/AotExample/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+ net10.0
+ net10.0
+
+
diff --git a/src/Mocha/examples/AotExample/docker-compose.yml b/src/Mocha/examples/AotExample/docker-compose.yml
new file mode 100644
index 00000000000..4765fea30da
--- /dev/null
+++ b/src/Mocha/examples/AotExample/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ rabbitmq:
+ image: rabbitmq:4-management
+ ports:
+ - "5673:5672"
+ - "15673:15672"
+ environment:
+ RABBITMQ_DEFAULT_USER: guest
+ RABBITMQ_DEFAULT_PASS: guest
diff --git a/src/Mocha/src/.gitignore b/src/Mocha/src/.gitignore
index 0e2b7f4aa32..1f4674a85bd 100644
--- a/src/Mocha/src/.gitignore
+++ b/src/Mocha/src/.gitignore
@@ -3,3 +3,5 @@ mocha-visualizer/*
!mocha-visualizer/package.json
!mocha-visualizer/README.md
!mocha-visualizer/dist/
+
+Generated
diff --git a/src/Mocha/src/Mocha.Abstractions/MessagingModuleAttribute.cs b/src/Mocha/src/Mocha.Abstractions/MessagingModuleAttribute.cs
index d594591e9b3..09721fa9a73 100644
--- a/src/Mocha/src/Mocha.Abstractions/MessagingModuleAttribute.cs
+++ b/src/Mocha/src/Mocha.Abstractions/MessagingModuleAttribute.cs
@@ -23,4 +23,10 @@ public MessagingModuleAttribute(string name)
/// Gets the module name.
///
public string Name { get; }
+
+ ///
+ /// Gets or sets the
+ /// type to use for AOT-compatible JSON serialization of messages and saga state.
+ ///
+ public Type? JsonContext { get; set; }
}
diff --git a/src/Mocha/src/Mocha.Abstractions/MessagingModuleInfoAttribute.cs b/src/Mocha/src/Mocha.Abstractions/MessagingModuleInfoAttribute.cs
new file mode 100644
index 00000000000..dd67a406534
--- /dev/null
+++ b/src/Mocha/src/Mocha.Abstractions/MessagingModuleInfoAttribute.cs
@@ -0,0 +1,25 @@
+namespace Mocha;
+
+///
+/// Annotates a generated messaging registration method with metadata about the message types
+/// it registers. This enables cross-project analyzers to discover types registered by
+/// referenced modules without scanning assemblies.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+public sealed class MessagingModuleInfoAttribute : Attribute
+{
+ ///
+ /// Gets or sets the message types registered by this module.
+ ///
+ public Type[] MessageTypes { get; set; } = [];
+
+ ///
+ /// Gets or sets the saga types registered by this module.
+ ///
+ public Type[] SagaTypes { get; set; } = [];
+
+ ///
+ /// Gets or sets the handler types registered by this module.
+ ///
+ public Type[] HandlerTypes { get; set; } = [];
+}
diff --git a/src/Mocha/src/Mocha.Abstractions/Mocha.Abstractions.csproj b/src/Mocha/src/Mocha.Abstractions/Mocha.Abstractions.csproj
index d2646770903..7b5b5568c2d 100644
--- a/src/Mocha/src/Mocha.Abstractions/Mocha.Abstractions.csproj
+++ b/src/Mocha/src/Mocha.Abstractions/Mocha.Abstractions.csproj
@@ -4,6 +4,7 @@
Mocha.Abstractions
enable
enable
+ true
diff --git a/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md
index 7415218a67e..d34419fbe27 100644
--- a/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/Mocha/src/Mocha.Analyzers/AnalyzerReleases.Unshipped.md
@@ -7,7 +7,12 @@ MO0002 | Mediator | Error | Message type has multiple handlers
MO0003 | Mediator | Warning | Handler is abstract and will not be registered
MO0004 | Mediator | Info | Open generic message type cannot be dispatched
MO0005 | Mediator | Error | Handler implements multiple mediator handler interfaces
+MO0006 | Mediator | Info | Open generic handler cannot be auto-registered
MO0011 | Messaging | Error | Request type has multiple handlers
MO0012 | Messaging | Info | Open generic messaging handler cannot be auto-registered
MO0013 | Messaging | Warning | Messaging handler is abstract
MO0014 | Messaging | Error | Saga must have a public parameterless constructor
+MO0015 | Messaging | Error | Missing JsonSerializerContext for AOT
+MO0016 | Messaging | Error | Missing JsonSerializable attribute
+MO0018 | Messaging | Warning | Type not in JsonSerializerContext
+MO0020 | Mediator | Warning | Command/query sent but no handler found
diff --git a/src/Mocha/src/Mocha.Analyzers/Errors.cs b/src/Mocha/src/Mocha.Analyzers/Errors.cs
index 1e25bb72393..875b0890d51 100644
--- a/src/Mocha/src/Mocha.Analyzers/Errors.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Errors.cs
@@ -84,6 +84,21 @@ public static class Errors
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ ///
+ /// Gets the descriptor for MO0006: a mediator handler is an open generic and cannot be auto-registered.
+ ///
+ ///
+ /// Reported as an info when a mediator handler has unbound type parameters,
+ /// making it impossible to register at compile time.
+ ///
+ public static readonly DiagnosticDescriptor OpenGenericHandler = new(
+ id: "MO0006",
+ title: "Open generic handler cannot be auto-registered",
+ messageFormat: "Handler '{0}' is an open generic and cannot be auto-registered",
+ category: "Mediator",
+ defaultSeverity: DiagnosticSeverity.Info,
+ isEnabledByDefault: true);
+
///
/// Gets the descriptor for MO0011: a request type has more than one handler.
///
@@ -143,4 +158,67 @@ public static class Errors
category: "Messaging",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
+
+ ///
+ /// Gets the descriptor for MO0015: a messaging module must specify a JsonSerializerContext
+ /// when publishing for AOT.
+ ///
+ ///
+ /// Reported as an error when PublishAot is true but the
+ /// [assembly: MessagingModule] attribute does not include a JsonContext property.
+ ///
+ public static readonly DiagnosticDescriptor MissingJsonSerializerContext = new(
+ id: "MO0015",
+ title: "Missing JsonSerializerContext for AOT",
+ messageFormat: "MessagingModule '{0}' must specify JsonContext when publishing for AOT. Add JsonContext = typeof(YourJsonContext) to the MessagingModule attribute.",
+ category: "Messaging",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Gets the descriptor for MO0016: a message type is not included in the JsonSerializerContext.
+ ///
+ ///
+ /// Reported as an error when a type is used as a message, request, response, or saga state
+ /// but is not declared via [JsonSerializable(typeof(...))] on the specified
+ /// JsonSerializerContext.
+ ///
+ public static readonly DiagnosticDescriptor MissingJsonSerializable = new(
+ id: "MO0016",
+ title: "Missing JsonSerializable attribute",
+ messageFormat: "Type '{0}' is used as a message type but is not included in JsonSerializerContext '{1}'. Add [JsonSerializable(typeof({0}))] to the context.",
+ category: "Messaging",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Gets the descriptor for MO0018: a type used at a call site is not in the JsonSerializerContext.
+ ///
+ ///
+ /// Reported as a warning when AOT publishing is enabled and a message type used in a
+ /// dispatch call is not declared via [JsonSerializable(typeof(...))] on the specified
+ /// JsonSerializerContext.
+ ///
+ public static readonly DiagnosticDescriptor CallSiteTypeNotInJsonContext = new(
+ id: "MO0018",
+ title: "Type not in JsonSerializerContext",
+ messageFormat: "Type '{0}' is used in a {1} call but is not included in JsonSerializerContext '{2}'. Add [JsonSerializable(typeof({0}))] to the context.",
+ category: "Messaging",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Gets the descriptor for MO0020: a command or query is sent but no handler was found.
+ ///
+ ///
+ /// Reported as a warning when a command or query type is dispatched via ISender
+ /// but no corresponding handler implementation is found in the compilation.
+ ///
+ public static readonly DiagnosticDescriptor CallSiteNoHandler = new(
+ id: "MO0020",
+ title: "Command/query sent but no handler found",
+ messageFormat: "Type '{0}' is sent via {1} but no handler was found in this assembly. Ensure a handler is registered.",
+ category: "Mediator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
}
diff --git a/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs b/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs
index 4b4f6244605..f7758d3aea1 100644
--- a/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs
+++ b/src/Mocha/src/Mocha.Analyzers/FileBuilders/DependencyInjectionFileBuilder.cs
@@ -32,8 +32,62 @@ public override void WriteBeginClass()
Writer.IncreaseIndent();
}
- public void WriteBeginRegistrationMethod()
+ ///
+ /// Writes the opening of the registration extension method, including the
+ /// [MediatorModuleInfo] attribute with the message and handler types arrays.
+ ///
+ ///
+ /// The fully qualified message type names to include in the attribute, or to omit.
+ ///
+ ///
+ /// The fully qualified handler type names to include in the attribute, or to omit.
+ ///
+ public void WriteBeginRegistrationMethod(
+ IReadOnlyList? messageTypeNames = null,
+ IReadOnlyList? handlerTypeNames = null)
{
+ var hasMessages = messageTypeNames is { Count: > 0 };
+ var hasHandlers = handlerTypeNames is { Count: > 0 };
+
+ if (hasMessages || hasHandlers)
+ {
+ // Build the list of property assignments to emit, so we can handle
+ // comma placement correctly (no trailing comma on the last property).
+ var properties = new List<(string Name, IReadOnlyList Types)>();
+
+ if (hasMessages)
+ {
+ properties.Add(("MessageTypes", messageTypeNames!));
+ }
+
+ if (hasHandlers)
+ {
+ properties.Add(("HandlerTypes", handlerTypeNames!));
+ }
+
+ Writer.WriteIndentedLine("[global::Mocha.Mediator.MediatorModuleInfo(");
+ Writer.IncreaseIndent();
+
+ for (var i = 0; i < properties.Count; i++)
+ {
+ var (name, types) = properties[i];
+ var isLast = i == properties.Count - 1;
+
+ Writer.WriteIndentedLine("{0} = new global::System.Type[]", name);
+ Writer.WriteIndentedLine("{");
+ Writer.IncreaseIndent();
+ foreach (var typeName in types)
+ {
+ Writer.WriteIndentedLine("typeof({0}),", typeName);
+ }
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine(isLast ? "}" : "},");
+ }
+
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine(")]");
+ }
+
Writer.WriteIndentedLine("public static global::Mocha.Mediator.IMediatorHostBuilder {0}(", _methodName);
Writer.IncreaseIndent();
Writer.WriteIndentedLine("this global::Mocha.Mediator.IMediatorHostBuilder builder)");
@@ -72,9 +126,7 @@ public void WriteHandlerConfiguration(HandlerInfo handler)
///
/// Writes an AddHandlerConfiguration call for a notification handler.
///
- public void WriteNotificationHandlerConfiguration(
- string notificationType,
- NotificationHandlerInfo handler)
+ public void WriteNotificationHandlerConfiguration(string notificationType, NotificationHandlerInfo handler)
{
WriteAddHandlerConfiguration(
handler.HandlerTypeName,
diff --git a/src/Mocha/src/Mocha.Analyzers/FileBuilders/MessagingDependencyInjectionFileBuilder.cs b/src/Mocha/src/Mocha.Analyzers/FileBuilders/MessagingDependencyInjectionFileBuilder.cs
index f5d32a07fb7..8795248926d 100644
--- a/src/Mocha/src/Mocha.Analyzers/FileBuilders/MessagingDependencyInjectionFileBuilder.cs
+++ b/src/Mocha/src/Mocha.Analyzers/FileBuilders/MessagingDependencyInjectionFileBuilder.cs
@@ -41,10 +41,71 @@ public override void WriteBeginClass()
}
///
- /// Writes the opening of the registration extension method.
+ /// Writes the opening of the registration extension method, including the
+ /// [MessagingModuleInfo] attribute with the message, saga, and handler types arrays.
///
- public void WriteBeginRegistrationMethod()
+ ///
+ /// The fully qualified message type names to include in the attribute, or to omit.
+ ///
+ ///
+ /// The fully qualified saga type names to include in the attribute, or to omit.
+ ///
+ ///
+ /// The fully qualified handler type names to include in the attribute, or to omit.
+ ///
+ public void WriteBeginRegistrationMethod(
+ IReadOnlyList? messageTypeNames = null,
+ IReadOnlyList? sagaTypeNames = null,
+ IReadOnlyList? handlerTypeNames = null)
{
+ var hasMessages = messageTypeNames is { Count: > 0 };
+ var hasSagas = sagaTypeNames is { Count: > 0 };
+ var hasHandlers = handlerTypeNames is { Count: > 0 };
+
+ if (hasMessages || hasSagas || hasHandlers)
+ {
+ // Build the list of property assignments to emit, so we can handle
+ // comma placement correctly (no trailing comma on the last property).
+ var properties = new List<(string Name, IReadOnlyList Types)>();
+
+ if (hasMessages)
+ {
+ properties.Add(("MessageTypes", messageTypeNames!));
+ }
+
+ if (hasSagas)
+ {
+ properties.Add(("SagaTypes", sagaTypeNames!));
+ }
+
+ if (hasHandlers)
+ {
+ properties.Add(("HandlerTypes", handlerTypeNames!));
+ }
+
+ Writer.WriteIndentedLine("[global::Mocha.MessagingModuleInfo(");
+ Writer.IncreaseIndent();
+
+ for (var i = 0; i < properties.Count; i++)
+ {
+ var (name, types) = properties[i];
+ var isLast = i == properties.Count - 1;
+
+ Writer.WriteIndentedLine("{0} = new global::System.Type[]", name);
+ Writer.WriteIndentedLine("{");
+ Writer.IncreaseIndent();
+ foreach (var typeName in types)
+ {
+ Writer.WriteIndentedLine("typeof({0}),", typeName);
+ }
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine(isLast ? "}" : "},");
+ }
+
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine(")]");
+ }
+
Writer.WriteIndentedLine("public static global::Mocha.IMessageBusHostBuilder {0}(", _methodName);
Writer.IncreaseIndent();
Writer.WriteIndentedLine("this global::Mocha.IMessageBusHostBuilder builder)");
@@ -53,6 +114,111 @@ public void WriteBeginRegistrationMethod()
Writer.IncreaseIndent();
}
+ ///
+ /// Writes the strict mode configuration that requires explicit message type registration.
+ ///
+ public void WriteStrictModeConfiguration()
+ {
+ Writer.WriteIndentedLine("global::Mocha.MessageBusHostBuilderExtensions.ModifyOptions(");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("builder,");
+ Writer.WriteIndentedLine("static o => o.IsAotCompatible = true);");
+ Writer.DecreaseIndent();
+ }
+
+ ///
+ /// Writes the registration of a JsonSerializerContext as a type info resolver.
+ ///
+ /// The fully qualified type name of the JsonSerializerContext.
+ public void WriteJsonTypeInfoResolverRegistration(string jsonContextTypeName)
+ {
+ Writer.WriteIndentedLine("global::Mocha.MessageBusHostBuilderExtensions.AddJsonTypeInfoResolver(");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("builder,");
+ Writer.WriteIndentedLine("{0}.Default);", jsonContextTypeName);
+ Writer.DecreaseIndent();
+ }
+
+ ///
+ /// Writes a message configuration registration with a pre-built JSON serializer.
+ ///
+ /// The fully qualified message type name.
+ /// The fully qualified type name of the JsonSerializerContext.
+ ///
+ /// The pre-computed enclosed types sorted by specificity, or to omit.
+ ///
+ public void WriteMessageConfiguration(
+ string messageTypeName,
+ string jsonContextTypeName,
+ List? enclosedTypes = null)
+ {
+ Writer.WriteIndentedLine("global::Mocha.MessageBusHostBuilderExtensions.AddMessageConfiguration(");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("builder,");
+ Writer.WriteIndentedLine("new global::Mocha.MessagingMessageConfiguration");
+ Writer.WriteIndentedLine("{");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("MessageType = typeof({0}),", messageTypeName);
+ Writer.WriteIndentedLine("Serializer = new global::Mocha.JsonMessageSerializer(");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("{0}.Default.GetTypeInfo(typeof({1}))!),", jsonContextTypeName, messageTypeName);
+ Writer.DecreaseIndent();
+
+ if (enclosedTypes is { Count: > 0 })
+ {
+ Writer.WriteIndentedLine("EnclosedTypes = new global::System.Type[]");
+ Writer.WriteIndentedLine("{");
+ Writer.IncreaseIndent();
+ foreach (var typeName in enclosedTypes)
+ {
+ Writer.WriteIndentedLine("typeof({0}),", typeName);
+ }
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine("},");
+ }
+
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine("});");
+ Writer.DecreaseIndent();
+ }
+
+ ///
+ /// Writes a saga configuration registration. When a
+ /// is provided, includes a pre-built JSON state serializer; otherwise emits a configuration
+ /// with only the saga type.
+ ///
+ /// The fully qualified saga type name.
+ /// The fully qualified saga state type name.
+ ///
+ /// The fully qualified type name of the JsonSerializerContext, or
+ /// to omit the state serializer.
+ ///
+ public void WriteSagaConfiguration(string sagaTypeName, string stateTypeName, string? jsonContextTypeName)
+ {
+ Writer.WriteIndentedLine("global::Mocha.MessageBusHostBuilderExtensions.AddSagaConfiguration<");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("{0}>(", sagaTypeName);
+ Writer.DecreaseIndent();
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("builder,");
+ Writer.WriteIndentedLine("new global::Mocha.MessagingSagaConfiguration");
+ Writer.WriteIndentedLine("{");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("SagaType = typeof({0}),", sagaTypeName);
+
+ if (jsonContextTypeName is not null)
+ {
+ Writer.WriteIndentedLine("StateSerializer = new global::Mocha.Sagas.JsonSagaStateSerializer(");
+ Writer.IncreaseIndent();
+ Writer.WriteIndentedLine("{0}.Default.GetTypeInfo(typeof({1}))!),", jsonContextTypeName, stateTypeName);
+ Writer.DecreaseIndent();
+ }
+
+ Writer.DecreaseIndent();
+ Writer.WriteIndentedLine("});");
+ Writer.DecreaseIndent();
+ }
+
///
/// Writes a handler registration call for the specified messaging handler.
///
@@ -61,16 +227,12 @@ public void WriteHandlerRegistration(MessagingHandlerInfo handler)
{
var factoryCall = handler.Kind switch
{
- MessagingHandlerKind.Event =>
- $"Subscribe<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
- MessagingHandlerKind.Send =>
- $"Send<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
+ MessagingHandlerKind.Event => $"Subscribe<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
+ MessagingHandlerKind.Send => $"Send<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
MessagingHandlerKind.RequestResponse =>
$"Request<{handler.HandlerTypeName}, {handler.MessageTypeName}, {handler.ResponseTypeName}>()",
- MessagingHandlerKind.Consumer =>
- $"Consume<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
- MessagingHandlerKind.Batch =>
- $"Batch<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
+ MessagingHandlerKind.Consumer => $"Consume<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
+ MessagingHandlerKind.Batch => $"Batch<{handler.HandlerTypeName}, {handler.MessageTypeName}>()",
_ => throw new ArgumentOutOfRangeException()
};
@@ -88,19 +250,6 @@ public void WriteHandlerRegistration(MessagingHandlerInfo handler)
Writer.DecreaseIndent();
}
- ///
- /// Writes a saga registration call for the specified saga.
- ///
- /// The saga info to register.
- public void WriteSagaRegistration(SagaInfo saga)
- {
- Writer.WriteIndentedLine(
- "global::Mocha.MessageBusHostBuilderExtensions.AddSaga<");
- Writer.IncreaseIndent();
- Writer.WriteIndentedLine("{0}>(builder);", saga.SagaTypeName);
- Writer.DecreaseIndent();
- }
-
///
/// Writes a section comment to visually separate groups of registrations.
///
diff --git a/src/Mocha/src/Mocha.Analyzers/Filters/InvocationCallSiteFilter.cs b/src/Mocha/src/Mocha.Analyzers/Filters/InvocationCallSiteFilter.cs
new file mode 100644
index 00000000000..ab6bfc4fd0b
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Filters/InvocationCallSiteFilter.cs
@@ -0,0 +1,41 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Mocha.Analyzers.Filters;
+
+///
+/// Provides a singleton that matches invocation expressions
+/// where the method name is one of the known message dispatch methods. This is a cheap
+/// syntactic check — no semantic analysis is performed.
+///
+public sealed class InvocationCallSiteFilter : ISyntaxFilter
+{
+ private InvocationCallSiteFilter() { }
+
+ ///
+ public bool IsMatch(SyntaxNode node)
+ => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccess }
+ && IsDispatchMethodName(GetMethodName(memberAccess));
+
+ ///
+ /// Gets the singleton instance of .
+ ///
+ public static InvocationCallSiteFilter Instance { get; } = new();
+
+ private static string? GetMethodName(MemberAccessExpressionSyntax memberAccess)
+ => memberAccess.Name switch
+ {
+ GenericNameSyntax generic => generic.Identifier.Text,
+ IdentifierNameSyntax identifier => identifier.Identifier.Text,
+ _ => null
+ };
+
+ private static bool IsDispatchMethodName(string? name)
+ => name
+ is "SendAsync"
+ or "PublishAsync"
+ or "QueryAsync"
+ or "ScheduleSendAsync"
+ or "SchedulePublishAsync"
+ or "RequestAsync";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Filters/InvocationModuleFilter.cs b/src/Mocha/src/Mocha.Analyzers/Filters/InvocationModuleFilter.cs
new file mode 100644
index 00000000000..9a9c1e8c4db
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Filters/InvocationModuleFilter.cs
@@ -0,0 +1,32 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Mocha.Analyzers.Filters;
+
+///
+/// Provides a singleton that matches invocation expressions
+/// where the method name starts with "Add". This is a cheap syntactic pre-screen for
+/// module registration methods like builder.AddOrderService().
+///
+public sealed class InvocationModuleFilter : ISyntaxFilter
+{
+ private InvocationModuleFilter() { }
+
+ ///
+ public bool IsMatch(SyntaxNode node)
+ => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccess }
+ && GetMethodName(memberAccess) is { } name
+ && name.StartsWith("Add", StringComparison.Ordinal);
+
+ ///
+ /// Gets the singleton instance of .
+ ///
+ public static InvocationModuleFilter Instance { get; } = new();
+
+ private static string? GetMethodName(MemberAccessExpressionSyntax memberAccess)
+ => memberAccess.Name switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.Text,
+ _ => null
+ };
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs
index 197cff1c0c9..b6087113b2c 100644
--- a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs
@@ -35,6 +35,35 @@ public void Generate(
return;
}
+ // Collect all unique message types for the [MediatorModuleInfo] attribute.
+ var allMessageTypeNames = new HashSet(StringComparer.Ordinal);
+
+ foreach (var handler in handlers)
+ {
+ allMessageTypeNames.Add(handler.MessageTypeName);
+
+ if (handler.ResponseTypeName is not null)
+ {
+ allMessageTypeNames.Add(handler.ResponseTypeName);
+ }
+ }
+
+ foreach (var handler in notificationHandlers)
+ {
+ allMessageTypeNames.Add(handler.NotificationTypeName);
+ }
+
+ var sortedMessageTypeNames = allMessageTypeNames
+ .OrderBy(t => t, StringComparer.Ordinal)
+ .ToList();
+
+ var sortedHandlerTypeNames = handlers
+ .Select(h => h.HandlerTypeName)
+ .Concat(notificationHandlers.Select(h => h.HandlerTypeName))
+ .Distinct(StringComparer.Ordinal)
+ .OrderBy(t => t, StringComparer.Ordinal)
+ .ToList();
+
using var builder = new DependencyInjectionFileBuilder(moduleName, assemblyName);
var notificationGroups = notificationHandlers
@@ -45,7 +74,7 @@ public void Generate(
builder.WriteHeader();
builder.WriteBeginNamespace();
builder.WriteBeginClass();
- builder.WriteBeginRegistrationMethod();
+ builder.WriteBeginRegistrationMethod(sortedMessageTypeNames, sortedHandlerTypeNames);
// Register all handler configurations
if (handlers.Count > 0 || notificationGroups.Count > 0)
diff --git a/src/Mocha/src/Mocha.Analyzers/Generators/MessagingDependencyInjectionGenerator.cs b/src/Mocha/src/Mocha.Analyzers/Generators/MessagingDependencyInjectionGenerator.cs
index 5b3ef33825b..0ad2c7354fe 100644
--- a/src/Mocha/src/Mocha.Analyzers/Generators/MessagingDependencyInjectionGenerator.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Generators/MessagingDependencyInjectionGenerator.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Mocha.Analyzers.FileBuilders;
+using Mocha.Analyzers.Utils;
namespace Mocha.Analyzers.Generators;
@@ -31,17 +32,206 @@ public void Generate(
.OrderBy(s => s.OrderByKey)
.ToList();
- if (handlers.Count == 0 && sagas.Count == 0)
+ var contextOnlyTypes = syntaxInfos
+ .OfType()
+ .OrderBy(c => c.OrderByKey)
+ .ToList();
+
+ if (handlers.Count == 0 && sagas.Count == 0 && contextOnlyTypes.Count == 0)
{
return;
}
+ // Find the module info to check for JsonContext.
+ string? jsonContextTypeName = null;
+
+ foreach (var info in syntaxInfos)
+ {
+ if (info is MessagingModuleInfo moduleInfo)
+ {
+ jsonContextTypeName = moduleInfo.JsonContextTypeName;
+ break;
+ }
+ }
+
+ // Collect type names imported from referenced modules — these already have
+ // serializer registrations from the referenced module's Add*() method.
+ var importedTypeNames = new HashSet(StringComparer.Ordinal);
+ var importedSagaTypeNames = new HashSet(StringComparer.Ordinal);
+ var importedHandlerTypeNames = new HashSet(StringComparer.Ordinal);
+
+ // Collect type names that are actually declared in the local JsonSerializerContext.
+ var jsonContextTypeNames = new HashSet(StringComparer.Ordinal);
+
+ foreach (var info in syntaxInfos)
+ {
+ if (info is ImportedModuleTypesInfo imported)
+ {
+ foreach (var typeName in imported.ImportedTypeNames)
+ {
+ importedTypeNames.Add(typeName);
+ }
+
+ foreach (var typeName in imported.ImportedSagaTypeNames)
+ {
+ importedSagaTypeNames.Add(typeName);
+ }
+
+ foreach (var typeName in imported.ImportedHandlerTypeNames)
+ {
+ importedHandlerTypeNames.Add(typeName);
+ }
+ }
+ else if (info is JsonContextSerializableTypesInfo jsonContextTypes)
+ {
+ foreach (var typeName in jsonContextTypes.TypeNames)
+ {
+ jsonContextTypeNames.Add(typeName);
+ }
+ }
+ }
+
+ // Collect all unique message types that this module actually registers
+ // serializers for via AddMessageConfiguration. Only types in the local
+ // JsonSerializerContext (excluding imports) qualify. This set is used for both
+ // the [MessagingModuleInfo] attribute and the actual serializer registrations.
+ var messageTypes = new HashSet(StringComparer.Ordinal);
+
+ if (jsonContextTypeName is not null)
+ {
+ foreach (var handler in handlers)
+ {
+ if (!importedTypeNames.Contains(handler.MessageTypeName)
+ && jsonContextTypeNames.Contains(handler.MessageTypeName))
+ {
+ messageTypes.Add(handler.MessageTypeName);
+ }
+
+ if (handler.ResponseTypeName is not null
+ && !importedTypeNames.Contains(handler.ResponseTypeName)
+ && jsonContextTypeNames.Contains(handler.ResponseTypeName))
+ {
+ messageTypes.Add(handler.ResponseTypeName);
+ }
+ }
+
+ foreach (var contextOnly in contextOnlyTypes)
+ {
+ if (!importedTypeNames.Contains(contextOnly.MessageTypeName))
+ {
+ messageTypes.Add(contextOnly.MessageTypeName);
+ }
+ }
+ }
+
+ var sortedMessageTypeNames = messageTypes
+ .OrderBy(t => t, StringComparer.Ordinal)
+ .ToList();
+
+ var sortedSagaTypeNames = sagas
+ .Select(s => s.SagaTypeName)
+ .Distinct(StringComparer.Ordinal)
+ .Where(t => !importedSagaTypeNames.Contains(t))
+ .OrderBy(t => t, StringComparer.Ordinal)
+ .ToList();
+
+ var sortedHandlerTypeNames = handlers
+ .Select(h => h.HandlerTypeName)
+ .Distinct(StringComparer.Ordinal)
+ .Where(t => !importedHandlerTypeNames.Contains(t))
+ .OrderBy(t => t, StringComparer.Ordinal)
+ .ToList();
+
using var builder = new MessagingDependencyInjectionFileBuilder(moduleName, assemblyName);
builder.WriteHeader();
builder.WriteBeginNamespace();
builder.WriteBeginClass();
- builder.WriteBeginRegistrationMethod();
+ builder.WriteBeginRegistrationMethod(sortedMessageTypeNames, sortedSagaTypeNames, sortedHandlerTypeNames);
+
+ // When JsonContext is specified, emit AOT registrations at the top of the method.
+ if (jsonContextTypeName is not null)
+ {
+ builder.WriteSectionComment("AOT Configuration");
+ builder.WriteStrictModeConfiguration();
+ builder.WriteJsonTypeInfoResolverRegistration(jsonContextTypeName);
+
+ // Compute enclosed types per message type (pre-sorted by specificity).
+ var enclosedTypesMap = new Dictionary>(StringComparer.Ordinal);
+
+ // Build a lookup from message type name to its full hierarchy.
+ var hierarchyLookup = new Dictionary>(StringComparer.Ordinal);
+ foreach (var handler in handlers)
+ {
+ if (!hierarchyLookup.ContainsKey(handler.MessageTypeName))
+ {
+ hierarchyLookup[handler.MessageTypeName] = handler.MessageTypeHierarchy;
+ }
+ }
+
+ foreach (var contextOnly in contextOnlyTypes)
+ {
+ if (!hierarchyLookup.ContainsKey(contextOnly.MessageTypeName))
+ {
+ hierarchyLookup[contextOnly.MessageTypeName] = contextOnly.MessageTypeHierarchy;
+ }
+ }
+
+ foreach (var messageTypeName in messageTypes)
+ {
+ // Start with the type itself.
+ var enclosed = new List { messageTypeName };
+
+ // Filter hierarchy to registered types only (exclude framework types).
+ if (hierarchyLookup.TryGetValue(messageTypeName, out var hierarchy))
+ {
+ foreach (var typeName in hierarchy)
+ {
+ if (messageTypes.Contains(typeName) && !enclosed.Contains(typeName))
+ {
+ enclosed.Add(typeName);
+ }
+ }
+ }
+
+ // Sort by specificity: most specific first.
+ // Type A is "more specific" than type B if B appears in A's hierarchy.
+ enclosed.Sort((a, b) =>
+ {
+ int ScoreOf(string typeName)
+ {
+ if (!hierarchyLookup.TryGetValue(typeName, out var h))
+ {
+ return 0;
+ }
+
+ return enclosed.Count(other => other != typeName && h.Contains(other));
+ }
+
+ return ScoreOf(b).CompareTo(ScoreOf(a));
+ });
+
+ enclosedTypesMap[messageTypeName] = enclosed;
+ }
+
+ builder.WriteSectionComment("Message Type Serializers");
+
+ foreach (var messageType in messageTypes.OrderBy(t => t, StringComparer.Ordinal))
+ {
+ enclosedTypesMap.TryGetValue(messageType, out var enclosedTypes);
+ builder.WriteMessageConfiguration(messageType, jsonContextTypeName, enclosedTypes);
+ }
+ }
+
+ if (sagas.Count > 0)
+ {
+ builder.WriteSectionComment("Saga Configuration");
+
+ foreach (var saga in sagas)
+ {
+ builder.WriteSagaConfiguration(saga.SagaTypeName, saga.StateTypeName, jsonContextTypeName);
+ }
+ }
var batchHandlers = handlers
.Where(h => h.Kind == MessagingHandlerKind.Batch)
@@ -103,16 +293,6 @@ public void Generate(
}
}
- if (sagas.Count > 0)
- {
- builder.WriteSectionComment("Sagas");
-
- foreach (var saga in sagas)
- {
- builder.WriteSagaRegistration(saga);
- }
- }
-
builder.WriteEndRegistrationMethod();
builder.WriteEndClass();
builder.WriteEndNamespace();
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/CallSiteMessageTypeInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/CallSiteMessageTypeInspector.cs
new file mode 100644
index 00000000000..202de48e19f
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/CallSiteMessageTypeInspector.cs
@@ -0,0 +1,287 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Mocha.Analyzers.Filters;
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers.Inspectors;
+
+///
+/// Inspects dispatch call sites (SendAsync, PublishAsync, RequestAsync,
+/// QueryAsync, etc.) on IMessageBus, ISender, and IPublisher
+/// to extract the compile-time message and response types being dispatched.
+///
+///
+///
+/// The extracted types feed downstream generators and analyzers that verify serializer
+/// registrations (MO0018) and handler existence (MO0020).
+///
+///
+/// Type extraction varies by method shape: for most generic methods the message type comes
+/// from the type argument T, but for RequestAsync<TResponse> the type
+/// argument is the response type - the message type is inferred from the first
+/// argument's compile-time type instead. Non-generic overloads (e.g.
+/// RequestAsync(object, …)) also fall back to argument-expression analysis.
+///
+///
+public sealed class CallSiteMessageTypeInspector : ISyntaxInspector
+{
+ ///
+ public ImmutableArray Filters { get; } = [InvocationCallSiteFilter.Instance];
+
+ ///
+ public IImmutableSet SupportedKinds { get; } = ImmutableHashSet.Create(SyntaxKind.InvocationExpression);
+
+ ///
+ public bool TryHandle(
+ KnownTypeSymbols knownSymbols,
+ SyntaxNode node,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken,
+ out SyntaxInfo? syntaxInfo)
+ {
+ syntaxInfo = null;
+
+ if (node is not InvocationExpressionSyntax invocation)
+ {
+ return false;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var symbolInfo = semanticModel.GetSymbolInfo(invocation, cancellationToken);
+ if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
+ {
+ return false;
+ }
+
+ var receiverType = methodSymbol.ContainingType?.OriginalDefinition;
+ if (receiverType is null)
+ {
+ return false;
+ }
+
+ // Try IMessageBus
+ if (knownSymbols.IMessageBus is not null
+ && SymbolEqualityComparer.Default.Equals(receiverType, knownSymbols.IMessageBus))
+ {
+ return TryHandleMessageBus(methodSymbol, invocation, semanticModel, cancellationToken, out syntaxInfo);
+ }
+
+ // Try ISender (Mediator)
+ if (knownSymbols.ISender is not null
+ && SymbolEqualityComparer.Default.Equals(receiverType, knownSymbols.ISender))
+ {
+ return TryHandleSender(methodSymbol, invocation, semanticModel, cancellationToken, out syntaxInfo);
+ }
+
+ // Try IPublisher (Mediator)
+ if (knownSymbols.IPublisher is not null
+ && SymbolEqualityComparer.Default.Equals(receiverType, knownSymbols.IPublisher))
+ {
+ return TryHandlePublisher(methodSymbol, invocation, out syntaxInfo);
+ }
+
+ return false;
+ }
+
+ private static bool TryHandleMessageBus(
+ IMethodSymbol methodSymbol,
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken,
+ out SyntaxInfo? syntaxInfo)
+ {
+ syntaxInfo = null;
+
+ var methodName = methodSymbol.Name;
+ CallSiteKind kind;
+
+ switch (methodName)
+ {
+ case "PublishAsync":
+ kind = CallSiteKind.Publish;
+ break;
+ case "SendAsync":
+ kind = CallSiteKind.Send;
+ break;
+ case "SchedulePublishAsync":
+ kind = CallSiteKind.SchedulePublish;
+ break;
+ case "ScheduleSendAsync":
+ kind = CallSiteKind.ScheduleSend;
+ break;
+ case "RequestAsync":
+ kind = CallSiteKind.Request;
+ break;
+ default:
+ return false;
+ }
+
+ // Non-generic RequestAsync(object, CT) - ack-only version.
+ // Fall back to argument-expression analysis to get the compile-time type.
+ if (kind == CallSiteKind.Request && methodSymbol.TypeArguments.Length == 0)
+ {
+ var argType = GetFirstArgumentType(invocation, semanticModel, cancellationToken);
+ if (argType is not null
+ && argType.SpecialType != SpecialType.System_Object
+ && !IsOpenTypeParameter(argType))
+ {
+ var argTypeName = argType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var argLocation = invocation.GetLocation().ToLocationInfo();
+ syntaxInfo = new CallSiteMessageTypeInfo(argTypeName, kind, argLocation);
+ return true;
+ }
+
+ return false;
+ }
+
+ // All other IMessageBus methods are generic - extract T from type arguments.
+ if (methodSymbol.TypeArguments.Length == 0)
+ {
+ return false;
+ }
+
+ var messageType = methodSymbol.TypeArguments[0];
+
+ // For RequestAsync(IEventRequest, ...), the type argument is the response.
+ // We want the message type (the first parameter's compile-time type) AND the response type.
+ if (kind == CallSiteKind.Request
+ && methodSymbol.Parameters.Length > 0
+ && methodSymbol.Parameters[0].Type is INamedTypeSymbol)
+ {
+ // The first parameter is IEventRequest or a concrete type implementing it.
+ // Get the compile-time type of the first argument expression.
+ var firstArgType = GetFirstArgumentType(invocation, semanticModel, cancellationToken);
+ if (firstArgType is not null && !IsOpenTypeParameter(firstArgType))
+ {
+ var requestTypeName = firstArgType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var location = invocation.GetLocation().ToLocationInfo();
+
+ // The response type is the type argument TResponse from RequestAsync.
+ string? responseTypeName = null;
+ if (methodSymbol.TypeArguments.Length > 0
+ && !IsOpenTypeParameter(methodSymbol.TypeArguments[0]))
+ {
+ responseTypeName = methodSymbol
+ .TypeArguments[0]
+ .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ }
+
+ syntaxInfo = new CallSiteMessageTypeInfo(requestTypeName, kind, location, responseTypeName);
+ return true;
+ }
+
+ return false;
+ }
+
+ if (IsOpenTypeParameter(messageType))
+ {
+ return false;
+ }
+
+ var typeName = messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var locationInfo = invocation.GetLocation().ToLocationInfo();
+ syntaxInfo = new CallSiteMessageTypeInfo(typeName, kind, locationInfo);
+ return true;
+ }
+
+ private static bool TryHandleSender(
+ IMethodSymbol methodSymbol,
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken,
+ out SyntaxInfo? syntaxInfo)
+ {
+ syntaxInfo = null;
+
+ var methodName = methodSymbol.Name;
+ CallSiteKind kind;
+
+ switch (methodName)
+ {
+ case "SendAsync":
+ kind = CallSiteKind.MediatorSend;
+ break;
+ case "QueryAsync":
+ kind = CallSiteKind.MediatorQuery;
+ break;
+ default:
+ return false;
+ }
+
+ // ISender methods take the message as the first parameter.
+ // For SendAsync(ICommand, CT) and SendAsync(ICommand, CT)
+ // and QueryAsync(IQuery, CT), get the first argument's type.
+ // Skip SendAsync(object, CT) - runtime dispatch, no static type info.
+ if (methodSymbol.Parameters.Length == 0)
+ {
+ return false;
+ }
+
+ var firstParamType = methodSymbol.Parameters[0].Type;
+ if (firstParamType.SpecialType == SpecialType.System_Object)
+ {
+ return false;
+ }
+
+ var messageType = GetFirstArgumentType(invocation, semanticModel, cancellationToken);
+ if (messageType is null || IsOpenTypeParameter(messageType))
+ {
+ return false;
+ }
+
+ var typeName = messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var locationInfo = invocation.GetLocation().ToLocationInfo();
+ syntaxInfo = new CallSiteMessageTypeInfo(typeName, kind, locationInfo);
+ return true;
+ }
+
+ private static bool TryHandlePublisher(
+ IMethodSymbol methodSymbol,
+ InvocationExpressionSyntax invocation,
+ out SyntaxInfo? syntaxInfo)
+ {
+ syntaxInfo = null;
+
+ if (methodSymbol.Name != "PublishAsync")
+ {
+ return false;
+ }
+
+ // Skip PublishAsync(object, CT) - runtime dispatch.
+ if (methodSymbol.TypeArguments.Length == 0)
+ {
+ return false;
+ }
+
+ var messageType = methodSymbol.TypeArguments[0];
+ if (IsOpenTypeParameter(messageType))
+ {
+ return false;
+ }
+
+ var typeName = messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var locationInfo = invocation.GetLocation().ToLocationInfo();
+ syntaxInfo = new CallSiteMessageTypeInfo(typeName, CallSiteKind.MediatorPublish, locationInfo);
+ return true;
+ }
+
+ private static ITypeSymbol? GetFirstArgumentType(
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken)
+ {
+ if (invocation.ArgumentList.Arguments.Count == 0)
+ {
+ return null;
+ }
+
+ var firstArgExpression = invocation.ArgumentList.Arguments[0].Expression;
+ var typeInfo = semanticModel.GetTypeInfo(firstArgExpression, cancellationToken);
+ return typeInfo.Type;
+ }
+
+ private static bool IsOpenTypeParameter(ITypeSymbol type) => type.TypeKind == TypeKind.TypeParameter;
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs
index 39d055af115..3e50ec9c81f 100644
--- a/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/HandlerInspector.cs
@@ -53,12 +53,29 @@ public bool TryHandle(
return false;
}
+ // Check for open generics (MO0006)
+ if (namedTypeSymbol is { IsGenericType: true, TypeParameters.Length: > 0 })
+ {
+ if (ImplementsAnyHandlerInterface(knownSymbols, namedTypeSymbol))
+ {
+ var handlerName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var locationInfo = typeDeclaration.Identifier.GetLocation().ToLocationInfo();
+ syntaxInfo = new OpenGenericHandlerDiagnosticInfo(handlerName)
+ {
+ Diagnostics = new([
+ new DiagnosticInfo(Errors.OpenGenericHandler.Id, locationInfo, new([handlerName]))
+ ])
+ };
+ return true;
+ }
+
+ return false;
+ }
+
foreach (var descriptor in s_handlerKinds)
{
var target = descriptor.GetTarget(knownSymbols);
- var implemented = target is not null
- ? namedTypeSymbol.FindImplementedInterface(target)
- : null;
+ var implemented = target is not null ? namedTypeSymbol.FindImplementedInterface(target) : null;
if (implemented is null)
{
@@ -82,8 +99,31 @@ public bool TryHandle(
return false;
}
+ private static bool ImplementsAnyHandlerInterface(KnownTypeSymbols knownSymbols, INamedTypeSymbol namedTypeSymbol)
+ {
+ return (
+ knownSymbols.ICommandHandlerVoid is not null
+ && namedTypeSymbol.FindImplementedInterface(knownSymbols.ICommandHandlerVoid) is not null)
+ || (
+ knownSymbols.ICommandHandlerResponse is not null
+ && namedTypeSymbol.FindImplementedInterface(knownSymbols.ICommandHandlerResponse) is not null)
+ || (
+ knownSymbols.IQueryHandler is not null
+ && namedTypeSymbol.FindImplementedInterface(knownSymbols.IQueryHandler) is not null);
+ }
+
private sealed record HandlerKindDescriptor(
Func GetTarget,
HandlerKind Kind,
bool HasResponse);
}
+
+///
+/// A diagnostic-only SyntaxInfo for MO0006 (open generic mediator handler).
+/// This is not used by code generators.
+///
+internal sealed record OpenGenericHandlerDiagnosticInfo(string HandlerTypeName) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => $"OpenGenericDiag:{HandlerTypeName}";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/ImportedMediatorModuleTypeInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/ImportedMediatorModuleTypeInspector.cs
new file mode 100644
index 00000000000..51ed2575233
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/ImportedMediatorModuleTypeInspector.cs
@@ -0,0 +1,115 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Mocha.Analyzers.Filters;
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers.Inspectors;
+
+///
+/// Inspects invocation expressions to discover calls to source-generated mediator module
+/// registration methods (e.g. builder.AddOrderService()) and extracts their declared
+/// message and handler types.
+///
+///
+///
+/// The source generator decorates each generated Add* extension method with a
+/// [MediatorModuleInfo(MessageTypes = new[] { typeof(A) }, HandlerTypes = new[] { typeof(B) })]
+/// attribute that lists every message and handler type the module registers. This inspector
+/// resolves the called method symbol, reads that attribute, and emits an
+/// containing the type names.
+///
+///
+/// Downstream validators use this to avoid reporting false-positive MO0001 and MO0020
+/// diagnostics for types whose handlers exist in a referenced module.
+///
+///
+/// The syntactic pre-filter () broadly matches any
+/// method starting with Add; this inspector then narrows to only those carrying
+/// the [MediatorModuleInfo] attribute.
+///
+///
+public sealed class ImportedMediatorModuleTypeInspector : ISyntaxInspector
+{
+ ///
+ public ImmutableArray Filters { get; } = [InvocationModuleFilter.Instance];
+
+ ///
+ public IImmutableSet SupportedKinds { get; } = ImmutableHashSet.Create(SyntaxKind.InvocationExpression);
+
+ ///
+ public bool TryHandle(
+ KnownTypeSymbols knownSymbols,
+ SyntaxNode node,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken,
+ out SyntaxInfo? syntaxInfo)
+ {
+ syntaxInfo = null;
+
+ if (node is not InvocationExpressionSyntax invocation)
+ {
+ return false;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var symbolInfo = semanticModel.GetSymbolInfo(invocation, cancellationToken);
+ if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
+ {
+ return false;
+ }
+
+ // Look for [MediatorModuleInfo] on the resolved method.
+ foreach (var attr in methodSymbol.GetAttributes())
+ {
+ if (attr.AttributeClass?.ToDisplayString() != SyntaxConstants.MediatorModuleInfoAttribute)
+ {
+ continue;
+ }
+
+ var messageTypeNames = new List();
+ var handlerTypeNames = new List();
+
+ foreach (var namedArg in attr.NamedArguments)
+ {
+ if (namedArg.Value.Kind != TypedConstantKind.Array)
+ {
+ continue;
+ }
+
+ List? targetList = namedArg.Key switch
+ {
+ SyntaxConstants.MessageTypesProperty => messageTypeNames,
+ SyntaxConstants.HandlerTypesProperty => handlerTypeNames,
+ _ => null
+ };
+
+ if (targetList is null)
+ {
+ continue;
+ }
+
+ foreach (var element in namedArg.Value.Values)
+ {
+ if (element.Value is INamedTypeSymbol typeSymbol)
+ {
+ targetList.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ }
+ }
+
+ if (messageTypeNames.Count > 0 || handlerTypeNames.Count > 0)
+ {
+ syntaxInfo = new ImportedMediatorModuleTypesInfo(
+ methodSymbol.Name,
+ new ImmutableEquatableArray(messageTypeNames),
+ new ImmutableEquatableArray(handlerTypeNames));
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/ImportedModuleTypeInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/ImportedModuleTypeInspector.cs
new file mode 100644
index 00000000000..9fe7bb91d68
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/ImportedModuleTypeInspector.cs
@@ -0,0 +1,117 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Mocha.Analyzers.Filters;
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers.Inspectors;
+
+///
+/// Inspects invocation expressions to discover calls to source-generated module registration
+/// methods (e.g. builder.AddOrderService()) and extracts their declared message types.
+///
+///
+///
+/// The source generator decorates each generated Add* extension method with a
+/// [MessagingModuleInfo(MessageTypes = new[] { typeof(A), typeof(B) })] attribute
+/// that lists every message type the module handles. This inspector resolves the called
+/// method symbol, reads that attribute, and emits an
+/// containing the type names.
+///
+///
+/// Downstream generators use this to avoid emitting duplicate serializer registrations for
+/// types already covered by a referenced module.
+///
+///
+/// The syntactic pre-filter () broadly matches any
+/// method starting with Add; this inspector then narrows to only those carrying
+/// the [MessagingModuleInfo] attribute.
+///
+///
+public sealed class ImportedModuleTypeInspector : ISyntaxInspector
+{
+ ///
+ public ImmutableArray Filters { get; } = [InvocationModuleFilter.Instance];
+
+ ///
+ public IImmutableSet SupportedKinds { get; } = ImmutableHashSet.Create(SyntaxKind.InvocationExpression);
+
+ ///
+ public bool TryHandle(
+ KnownTypeSymbols knownSymbols,
+ SyntaxNode node,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken,
+ out SyntaxInfo? syntaxInfo)
+ {
+ syntaxInfo = null;
+
+ if (node is not InvocationExpressionSyntax invocation)
+ {
+ return false;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var symbolInfo = semanticModel.GetSymbolInfo(invocation, cancellationToken);
+ if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
+ {
+ return false;
+ }
+
+ // Look for [MessagingModuleInfo] on the resolved method.
+ foreach (var attr in methodSymbol.GetAttributes())
+ {
+ if (attr.AttributeClass?.ToDisplayString() != SyntaxConstants.MessagingModuleInfoAttribute)
+ {
+ continue;
+ }
+
+ var messageTypeNames = new List();
+ var sagaTypeNames = new List();
+ var handlerTypeNames = new List();
+
+ foreach (var namedArg in attr.NamedArguments)
+ {
+ if (namedArg.Value.Kind != TypedConstantKind.Array)
+ {
+ continue;
+ }
+
+ List? targetList = namedArg.Key switch
+ {
+ SyntaxConstants.MessageTypesProperty => messageTypeNames,
+ SyntaxConstants.SagaTypesProperty => sagaTypeNames,
+ SyntaxConstants.HandlerTypesProperty => handlerTypeNames,
+ _ => null
+ };
+
+ if (targetList is null)
+ {
+ continue;
+ }
+
+ foreach (var element in namedArg.Value.Values)
+ {
+ if (element.Value is INamedTypeSymbol typeSymbol)
+ {
+ targetList.Add(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ }
+ }
+
+ if (messageTypeNames.Count > 0 || sagaTypeNames.Count > 0 || handlerTypeNames.Count > 0)
+ {
+ syntaxInfo = new ImportedModuleTypesInfo(
+ methodSymbol.Name,
+ new ImmutableEquatableArray(messageTypeNames),
+ new ImmutableEquatableArray(sagaTypeNames),
+ new ImmutableEquatableArray(handlerTypeNames));
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingHandlerInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingHandlerInspector.cs
index e0f6ceed5e9..54a756bee9c 100644
--- a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingHandlerInspector.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingHandlerInspector.cs
@@ -88,10 +88,23 @@ public bool TryHandle(
var handlerFullName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var handlerNamespace = namedTypeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty;
- var messageTypeName = implemented.TypeArguments[descriptor.MessageTypeArgIndex]
- .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var messageSymbol = (INamedTypeSymbol)implemented.TypeArguments[descriptor.MessageTypeArgIndex];
+ var messageTypeName = messageSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var locationInfo = typeDeclaration.Identifier.GetLocation().ToLocationInfo();
+ // Walk the full type hierarchy (base types + interfaces) for AOT enclosed type computation.
+ var hierarchy = new List();
+ var currentBase = messageSymbol.BaseType;
+ while (currentBase is not null && currentBase.SpecialType != SpecialType.System_Object)
+ {
+ hierarchy.Add(currentBase.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ currentBase = currentBase.BaseType;
+ }
+ foreach (var iface in messageSymbol.AllInterfaces)
+ {
+ hierarchy.Add(iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+
syntaxInfo = new MessagingHandlerInfo(
handlerFullName,
handlerNamespace,
@@ -100,6 +113,7 @@ public bool TryHandle(
? implemented.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
: null,
descriptor.Kind,
+ new ImmutableEquatableArray(hierarchy),
locationInfo);
return true;
}
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingModuleInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingModuleInspector.cs
index d02a56ab860..ef2f4b58253 100644
--- a/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingModuleInspector.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/MessagingModuleInspector.cs
@@ -53,7 +53,25 @@ public bool TryHandle(
continue;
}
- syntaxInfo = new MessagingModuleInfo(name);
+ string? jsonContextTypeName = null;
+
+ // Look for the JsonContext named argument:
+ // [assembly: MessagingModule("MyApp", JsonContext = typeof(MyJsonContext))]
+ foreach (var arg in attributeSyntax.ArgumentList.Arguments)
+ {
+ if (arg.NameEquals is { Name.Identifier.Text: SyntaxConstants.JsonContextProperty }
+ && arg.Expression is TypeOfExpressionSyntax typeOfExpr)
+ {
+ var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, cancellationToken);
+ if (typeInfo.Type is not null)
+ {
+ jsonContextTypeName = typeInfo.Type.ToDisplayString(
+ SymbolDisplayFormat.FullyQualifiedFormat);
+ }
+ }
+ }
+
+ syntaxInfo = new MessagingModuleInfo(name, jsonContextTypeName);
return true;
}
}
diff --git a/src/Mocha/src/Mocha.Analyzers/Inspectors/SagaInspector.cs b/src/Mocha/src/Mocha.Analyzers/Inspectors/SagaInspector.cs
index 173f776967d..1a3496d17ba 100644
--- a/src/Mocha/src/Mocha.Analyzers/Inspectors/SagaInspector.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Inspectors/SagaInspector.cs
@@ -52,19 +52,20 @@ public bool TryHandle(
}
// Walk the base type chain looking for Saga
- if (!DerivesFromSaga(namedTypeSymbol, knownSymbols.Saga))
+ if (!TryGetSagaStateType(namedTypeSymbol, knownSymbols.Saga, out var stateType))
{
return false;
}
var sagaFullName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var sagaNamespace = namedTypeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty;
+ var stateTypeName = stateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Check for public parameterless constructor (MO0014)
if (!HasPublicParameterlessConstructor(namedTypeSymbol))
{
var locationInfo = typeDeclaration.Identifier.GetLocation().ToLocationInfo();
- syntaxInfo = new SagaInfo(sagaFullName, sagaNamespace)
+ syntaxInfo = new SagaInfo(sagaFullName, sagaNamespace, stateTypeName)
{
Diagnostics = new([
new DiagnosticInfo(
@@ -76,11 +77,14 @@ public bool TryHandle(
return true;
}
- syntaxInfo = new SagaInfo(sagaFullName, sagaNamespace);
+ syntaxInfo = new SagaInfo(sagaFullName, sagaNamespace, stateTypeName);
return true;
}
- private static bool DerivesFromSaga(INamedTypeSymbol type, INamedTypeSymbol sagaSymbol)
+ private static bool TryGetSagaStateType(
+ INamedTypeSymbol type,
+ INamedTypeSymbol sagaSymbol,
+ out ITypeSymbol stateType)
{
var current = type.BaseType;
while (current is not null)
@@ -88,12 +92,14 @@ private static bool DerivesFromSaga(INamedTypeSymbol type, INamedTypeSymbol saga
if (current.IsGenericType
&& SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, sagaSymbol))
{
+ stateType = current.TypeArguments[0];
return true;
}
current = current.BaseType;
}
+ stateType = null!;
return false;
}
diff --git a/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs b/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs
index b42a2b84fa9..479372af83e 100644
--- a/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs
+++ b/src/Mocha/src/Mocha.Analyzers/KnownTypeSymbols.cs
@@ -30,6 +30,9 @@ public sealed class KnownTypeSymbols
private Resolved? _saga;
private Resolved? _eventRequest;
private Resolved? _eventRequestOfT;
+ private Resolved? _messageBus;
+ private Resolved? _sender;
+ private Resolved? _publisher;
private KnownTypeSymbols(Compilation compilation)
{
@@ -133,6 +136,24 @@ public INamedTypeSymbol? IEventRequest
public INamedTypeSymbol? IEventRequestOfT
=> Resolve(SyntaxConstants.IEventRequestOfT, ref _eventRequestOfT);
+ ///
+ /// Gets the symbol for the IMessageBus interface.
+ ///
+ public INamedTypeSymbol? IMessageBus
+ => Resolve(SyntaxConstants.IMessageBus, ref _messageBus);
+
+ ///
+ /// Gets the symbol for the ISender interface (Mediator).
+ ///
+ public INamedTypeSymbol? ISender
+ => Resolve(SyntaxConstants.ISender, ref _sender);
+
+ ///
+ /// Gets the symbol for the IPublisher interface (Mediator).
+ ///
+ public INamedTypeSymbol? IPublisher
+ => Resolve(SyntaxConstants.IPublisher, ref _publisher);
+
private INamedTypeSymbol? Resolve(string metadataName, ref Resolved? field)
{
var snapshot = Interlocked.CompareExchange(ref field, null, null);
diff --git a/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs b/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs
index 51735b99681..38bc5b5b636 100644
--- a/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs
+++ b/src/Mocha/src/Mocha.Analyzers/MediatorGenerator.cs
@@ -28,14 +28,22 @@ public sealed class MediatorGenerator : IIncrementalGenerator
new MediatorModuleInspector()
];
+ private static readonly ISyntaxInspector[] s_callSiteInspectors =
+ [
+ new CallSiteMessageTypeInspector(),
+ new ImportedMediatorModuleTypeInspector()
+ ];
+
private static readonly ISyntaxGenerator[] s_generators =
[
new DependencyInjectionGenerator()
];
private static readonly Dictionary> s_inspectorLookup;
+ private static readonly Dictionary> s_callSiteInspectorLookup;
private static readonly Func s_predicate;
+ private static readonly Func s_callSitePredicate;
static MediatorGenerator()
{
@@ -65,6 +73,34 @@ static MediatorGenerator()
{
s_inspectorLookup[kvp.Key] = kvp.Value.ToImmutableArray();
}
+
+ // Build call-site filter + inspector lookup.
+ var callSiteFilterBuilder = new SyntaxFilterBuilder();
+ var callSiteInspectorLookup = new Dictionary>();
+
+ foreach (var inspector in s_callSiteInspectors)
+ {
+ callSiteFilterBuilder.AddRange(inspector.Filters);
+
+ foreach (var supportedKind in inspector.SupportedKinds)
+ {
+ if (!callSiteInspectorLookup.TryGetValue(supportedKind, out var inspectors))
+ {
+ inspectors = [];
+ callSiteInspectorLookup[supportedKind] = inspectors;
+ }
+
+ inspectors.Add(inspector);
+ }
+ }
+
+ s_callSitePredicate = callSiteFilterBuilder.Build();
+ s_callSiteInspectorLookup = new Dictionary>();
+
+ foreach (var kvp in callSiteInspectorLookup)
+ {
+ s_callSiteInspectorLookup[kvp.Key] = kvp.Value.ToImmutableArray();
+ }
}
///
@@ -80,11 +116,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Collect()
.WithTrackingName("MochaCollectedInfos");
+ var callSiteInfos = context
+ .SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (s, _) => s_callSitePredicate(s),
+ transform: static (ctx, ct) => TransformCallSite(ctx.Node, ctx.SemanticModel, ct))
+ .WhereNotNull()
+ .WithComparer(EqualityComparer.Default)
+ .WithTrackingName("MochaMediatorCallSiteInfos")
+ .Collect()
+ .WithTrackingName("MochaMediatorCollectedCallSiteInfos");
+
var assemblyName = context.CompilationProvider.Select(static (c, _) => c.AssemblyName ?? "Unknown");
context.RegisterSourceOutput(
- assemblyName.Combine(syntaxInfos),
- static (context, source) => Execute(context, source.Left, source.Right));
+ assemblyName.Combine(syntaxInfos).Combine(callSiteInfos),
+ static (context, source) => Execute(context, source.Left.Left, source.Left.Right, source.Right));
}
private static SyntaxInfo? Transform(
@@ -110,10 +156,34 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return null;
}
+ private static SyntaxInfo? TransformCallSite(
+ SyntaxNode node,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken)
+ {
+ if (!s_callSiteInspectorLookup.TryGetValue(node.Kind(), out var inspectors))
+ {
+ return null;
+ }
+
+ var knownTypeSymbols = KnownTypeSymbols.GetOrCreate(semanticModel.Compilation);
+
+ foreach (var inspector in inspectors)
+ {
+ if (inspector.TryHandle(knownTypeSymbols, node, semanticModel, cancellationToken, out var syntaxInfo))
+ {
+ return syntaxInfo;
+ }
+ }
+
+ return null;
+ }
+
private static void Execute(
SourceProductionContext context,
string assemblyName,
- ImmutableArray syntaxInfos)
+ ImmutableArray syntaxInfos,
+ ImmutableArray callSiteInfos)
{
var sourceFiles = PooledObjects.GetStringDictionary();
var moduleInfo = GetModuleInfo(syntaxInfos, ModuleNameHelper.CreateModuleName(assemblyName));
@@ -134,7 +204,10 @@ private static void Execute(
}
// Validate message types vs handlers (MO0001, MO0002)
- ValidateMessageHandlerPairing(context, syntaxInfos);
+ ValidateMessageHandlerPairing(context, syntaxInfos, callSiteInfos);
+
+ // Validate call-site types vs handlers (MO0020)
+ ValidateCallSiteNoHandler(context, syntaxInfos, callSiteInfos);
foreach (var generator in s_generators)
{
@@ -177,7 +250,8 @@ private static MediatorModuleInfo GetModuleInfo(ImmutableArray synta
private static void ValidateMessageHandlerPairing(
SourceProductionContext context,
- ImmutableArray syntaxInfos)
+ ImmutableArray syntaxInfos,
+ ImmutableArray callSiteInfos)
{
var messageTypes = new List();
var handlers = new List();
@@ -199,6 +273,20 @@ private static void ValidateMessageHandlerPairing(
return;
}
+ // Collect handler message type names from imported mediator modules.
+ var importedHandlerMessageTypes = new HashSet(StringComparer.Ordinal);
+
+ foreach (var info in callSiteInfos)
+ {
+ if (info is ImportedMediatorModuleTypesInfo imported)
+ {
+ foreach (var typeName in imported.ImportedTypeNames)
+ {
+ importedHandlerMessageTypes.Add(typeName);
+ }
+ }
+ }
+
// Build a lookup of handlers by message type name
var handlersByMessageType = new Dictionary>();
foreach (var handler in handlers)
@@ -219,6 +307,12 @@ private static void ValidateMessageHandlerPairing(
if (!handlersByMessageType.TryGetValue(messageType.MessageTypeName, out var matchingHandlers)
|| matchingHandlers.Count == 0)
{
+ // If the handler exists in an imported module, skip MO0001.
+ if (importedHandlerMessageTypes.Contains(messageType.MessageTypeName))
+ {
+ continue;
+ }
+
// MO0001: Missing handler
context.ReportDiagnostic(
Diagnostic.Create(Errors.MissingHandler, location, messageType.MessageTypeName));
@@ -239,13 +333,68 @@ private static void ValidateMessageHandlerPairing(
}
}
+ private static void ValidateCallSiteNoHandler(
+ SourceProductionContext context,
+ ImmutableArray syntaxInfos,
+ ImmutableArray callSiteInfos)
+ {
+ if (callSiteInfos.Length == 0)
+ {
+ return;
+ }
+
+ // Build a set of handler message type names from discovered handlers.
+ var handlerMessageTypes = new HashSet(StringComparer.Ordinal);
+
+ foreach (var info in syntaxInfos)
+ {
+ if (info is HandlerInfo { Diagnostics.Count: 0 } handler)
+ {
+ handlerMessageTypes.Add(handler.MessageTypeName);
+ }
+ }
+
+ // Include handler message types from imported mediator modules.
+ foreach (var info in callSiteInfos)
+ {
+ if (info is ImportedMediatorModuleTypesInfo imported)
+ {
+ foreach (var typeName in imported.ImportedTypeNames)
+ {
+ handlerMessageTypes.Add(typeName);
+ }
+ }
+ }
+
+ // Check each call-site for MediatorSend or MediatorQuery — not MediatorPublish.
+ foreach (var info in callSiteInfos)
+ {
+ if (info is CallSiteMessageTypeInfo
+ {
+ Kind: CallSiteKind.MediatorSend or CallSiteKind.MediatorQuery
+ } callSite
+ && !handlerMessageTypes.Contains(callSite.MessageTypeName))
+ {
+ var location = ReconstructLocation(callSite.Location);
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.CallSiteNoHandler,
+ location,
+ callSite.MessageTypeName,
+ callSite.Kind.ToString()));
+ }
+ }
+ }
+
private static readonly Dictionary s_descriptorLookup = new()
{
[Errors.MissingHandler.Id] = Errors.MissingHandler,
[Errors.DuplicateHandler.Id] = Errors.DuplicateHandler,
[Errors.AbstractHandler.Id] = Errors.AbstractHandler,
[Errors.OpenGenericMessageType.Id] = Errors.OpenGenericMessageType,
- [Errors.MultipleHandlerInterfaces.Id] = Errors.MultipleHandlerInterfaces
+ [Errors.MultipleHandlerInterfaces.Id] = Errors.MultipleHandlerInterfaces,
+ [Errors.OpenGenericHandler.Id] = Errors.OpenGenericHandler,
+ [Errors.CallSiteNoHandler.Id] = Errors.CallSiteNoHandler
};
private static Diagnostic ReconstructDiagnostic(DiagnosticInfo info)
diff --git a/src/Mocha/src/Mocha.Analyzers/MessagingGenerator.cs b/src/Mocha/src/Mocha.Analyzers/MessagingGenerator.cs
index e789c3e0c5a..b02916e51de 100644
--- a/src/Mocha/src/Mocha.Analyzers/MessagingGenerator.cs
+++ b/src/Mocha/src/Mocha.Analyzers/MessagingGenerator.cs
@@ -25,14 +25,22 @@ public sealed class MessagingGenerator : IIncrementalGenerator
new SagaInspector()
];
+ private static readonly ISyntaxInspector[] s_callSiteInspectors =
+ [
+ new CallSiteMessageTypeInspector(),
+ new ImportedModuleTypeInspector()
+ ];
+
private static readonly ISyntaxGenerator[] s_generators =
[
new MessagingDependencyInjectionGenerator()
];
private static readonly Dictionary> s_inspectorLookup;
+ private static readonly Dictionary> s_callSiteInspectorLookup;
private static readonly Func s_predicate;
+ private static readonly Func s_callSitePredicate;
static MessagingGenerator()
{
@@ -62,6 +70,34 @@ static MessagingGenerator()
{
s_inspectorLookup[kvp.Key] = kvp.Value.ToImmutableArray();
}
+
+ // Build call-site filter + inspector lookup.
+ var callSiteFilterBuilder = new SyntaxFilterBuilder();
+ var callSiteInspectorLookup = new Dictionary>();
+
+ foreach (var inspector in s_callSiteInspectors)
+ {
+ callSiteFilterBuilder.AddRange(inspector.Filters);
+
+ foreach (var supportedKind in inspector.SupportedKinds)
+ {
+ if (!callSiteInspectorLookup.TryGetValue(supportedKind, out var inspectors))
+ {
+ inspectors = [];
+ callSiteInspectorLookup[supportedKind] = inspectors;
+ }
+
+ inspectors.Add(inspector);
+ }
+ }
+
+ s_callSitePredicate = callSiteFilterBuilder.Build();
+ s_callSiteInspectorLookup = new Dictionary>();
+
+ foreach (var kvp in callSiteInspectorLookup)
+ {
+ s_callSiteInspectorLookup[kvp.Key] = kvp.Value.ToImmutableArray();
+ }
}
///
@@ -77,11 +113,43 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Collect()
.WithTrackingName("MochaMessagingCollectedInfos");
+ var callSiteInfos = context
+ .SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (s, _) => s_callSitePredicate(s),
+ transform: static (ctx, ct) => TransformCallSite(ctx.Node, ctx.SemanticModel, ct))
+ .WhereNotNull()
+ .WithComparer(EqualityComparer.Default)
+ .WithTrackingName("MochaCallSiteInfos")
+ .Collect()
+ .WithTrackingName("MochaCollectedCallSiteInfos");
+
var assemblyName = context.CompilationProvider.Select(static (c, _) => c.AssemblyName ?? "Unknown");
+ var isAotPublish = context.AnalyzerConfigOptionsProvider.Select(static (options, _) =>
+ {
+ options.GlobalOptions.TryGetValue("build_property.PublishAot", out var publishAot);
+ return string.Equals(publishAot, "true", StringComparison.OrdinalIgnoreCase);
+ });
+
+ // Extract JsonContext data from the Compilation into an equatable model so that
+ // CompilationProvider (which is never value-equal) does not flow into RegisterSourceOutput.
+ var jsonContextInfo = syntaxInfos
+ .Combine(context.CompilationProvider)
+ .Select(static (source, ct) => ExtractJsonContextInfo(source.Left, source.Right, ct));
+
context.RegisterSourceOutput(
- assemblyName.Combine(syntaxInfos),
- static (context, source) => Execute(context, source.Left, source.Right));
+ assemblyName
+ .Combine(syntaxInfos)
+ .Combine(callSiteInfos)
+ .Combine(isAotPublish)
+ .Combine(jsonContextInfo),
+ static (context, source) => Execute(
+ context,
+ source.Left.Left.Left.Left,
+ source.Left.Left.Left.Right,
+ source.Left.Left.Right,
+ source.Left.Right,
+ source.Right));
}
private static SyntaxInfo? Transform(
@@ -107,10 +175,36 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return null;
}
+ private static SyntaxInfo? TransformCallSite(
+ SyntaxNode node,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken)
+ {
+ if (!s_callSiteInspectorLookup.TryGetValue(node.Kind(), out var inspectors))
+ {
+ return null;
+ }
+
+ var knownTypeSymbols = KnownTypeSymbols.GetOrCreate(semanticModel.Compilation);
+
+ foreach (var inspector in inspectors)
+ {
+ if (inspector.TryHandle(knownTypeSymbols, node, semanticModel, cancellationToken, out var syntaxInfo))
+ {
+ return syntaxInfo;
+ }
+ }
+
+ return null;
+ }
+
private static void Execute(
SourceProductionContext context,
string assemblyName,
- ImmutableArray syntaxInfos)
+ ImmutableArray syntaxInfos,
+ ImmutableArray callSiteInfos,
+ bool isAotPublish,
+ JsonContextInfo jsonContextInfo)
{
var sourceFiles = PooledObjects.GetStringDictionary();
var moduleInfo = GetModuleInfo(syntaxInfos, ModuleNameHelper.CreateModuleName(assemblyName));
@@ -132,13 +226,40 @@ private static void Execute(
// Validate request handler pairing (MO0011)
ValidateRequestHandlerPairing(context, syntaxInfos);
+ // Validate AOT JsonSerializerContext coverage (MO0015, MO0016)
+ ValidateAotJsonContext(context, syntaxInfos, callSiteInfos, moduleInfo, isAotPublish, jsonContextInfo);
+
+ // Validate call-site types against JsonSerializerContext (MO0018)
+ ValidateCallSiteJsonContext(context, callSiteInfos, moduleInfo, isAotPublish, jsonContextInfo);
+
+ // Extract context-only message types from JsonSerializerContext (types without handlers).
+ var augmentedInfos = ExtractContextOnlyTypes(syntaxInfos, moduleInfo, jsonContextInfo);
+
+ // Pass the full set of JsonContext-serializable type names so the DI generator
+ // can restrict serializer registrations to types that are actually in the context.
+ if (jsonContextInfo.SerializableTypes.Count > 0)
+ {
+ var serializableTypeNames = new ImmutableEquatableArray(
+ jsonContextInfo.SerializableTypes.Select(t => t.TypeName));
+ augmentedInfos = augmentedInfos.Add(new JsonContextSerializableTypesInfo(serializableTypeNames));
+ }
+
+ // Include ImportedModuleTypesInfo entries so the DI generator can skip
+ // serializer registration for types already covered by referenced modules.
+ var importedModuleInfos = callSiteInfos.OfType().ToImmutableArray();
+
+ if (importedModuleInfos.Length > 0)
+ {
+ augmentedInfos = augmentedInfos.AddRange(importedModuleInfos.CastArray());
+ }
+
foreach (var generator in s_generators)
{
generator.Generate(
context,
assemblyName,
moduleInfo.ModuleName,
- syntaxInfos,
+ augmentedInfos,
AddSource);
}
@@ -164,13 +285,93 @@ private static MessagingModuleInfo GetModuleInfo(ImmutableArray synt
{
if (syntaxInfo is MessagingModuleInfo module)
{
- return new MessagingModuleInfo(ModuleNameHelper.SanitizeIdentifier(module.ModuleName));
+ return new MessagingModuleInfo(
+ ModuleNameHelper.SanitizeIdentifier(module.ModuleName),
+ module.JsonContextTypeName);
}
}
return new MessagingModuleInfo(defaultModuleName);
}
+ private static JsonContextInfo ExtractJsonContextInfo(
+ ImmutableArray syntaxInfos,
+ Compilation compilation,
+ CancellationToken cancellationToken)
+ {
+ // Find the module info to get the JsonContext type name.
+ string? jsonContextTypeName = null;
+
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is MessagingModuleInfo module)
+ {
+ jsonContextTypeName = module.JsonContextTypeName;
+ break;
+ }
+ }
+
+ if (jsonContextTypeName is null)
+ {
+ return new JsonContextInfo(null, ImmutableEquatableArray.Empty);
+ }
+
+ var metadataName = jsonContextTypeName.StartsWith("global::", StringComparison.Ordinal)
+ ? jsonContextTypeName.Substring("global::".Length)
+ : jsonContextTypeName;
+
+ var contextSymbol = compilation.GetTypeByMetadataName(metadataName);
+
+ if (contextSymbol is null)
+ {
+ return new JsonContextInfo(jsonContextTypeName, ImmutableEquatableArray.Empty);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var serializableTypes = new List();
+
+ foreach (var attr in contextSymbol.GetAttributes())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (attr.AttributeClass?.Name != "JsonSerializableAttribute"
+ || attr.ConstructorArguments.Length == 0
+ || attr.ConstructorArguments[0].Value is not INamedTypeSymbol serializableType)
+ {
+ continue;
+ }
+
+ var typeName = serializableType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ // Walk the full type hierarchy (base types + interfaces), same as MessagingHandlerInspector.
+ var hierarchy = new List();
+ var currentBase = serializableType.BaseType;
+
+ while (currentBase is not null && currentBase.SpecialType != SpecialType.System_Object)
+ {
+ hierarchy.Add(currentBase.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ currentBase = currentBase.BaseType;
+ }
+
+ foreach (var iface in serializableType.AllInterfaces)
+ {
+ hierarchy.Add(iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+
+ var location = attr.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation().ToLocationInfo();
+
+ serializableTypes.Add(new JsonSerializableTypeInfo(
+ typeName,
+ new ImmutableEquatableArray(hierarchy),
+ location));
+ }
+
+ return new JsonContextInfo(
+ jsonContextTypeName,
+ new ImmutableEquatableArray(serializableTypes));
+ }
+
private static void ValidateRequestHandlerPairing(
SourceProductionContext context,
ImmutableArray syntaxInfos)
@@ -226,12 +427,267 @@ private static void ValidateRequestHandlerPairing(
}
}
+ private static void ValidateAotJsonContext(
+ SourceProductionContext context,
+ ImmutableArray syntaxInfos,
+ ImmutableArray callSiteInfos,
+ MessagingModuleInfo moduleInfo,
+ bool isAotPublish,
+ JsonContextInfo jsonContextInfo)
+ {
+ if (!isAotPublish && jsonContextInfo.JsonContextTypeName is null)
+ {
+ return;
+ }
+
+ // Collect type names imported from referenced modules via [MessagingModuleInfo].
+ var importedTypes = new HashSet(StringComparer.Ordinal);
+
+ foreach (var info in callSiteInfos)
+ {
+ if (info is ImportedModuleTypesInfo imported)
+ {
+ foreach (var typeName in imported.ImportedTypeNames)
+ {
+ importedTypes.Add(typeName);
+ }
+ }
+ }
+
+ if (jsonContextInfo.JsonContextTypeName is null)
+ {
+ // Only fire MO0015 if this module has handlers or sagas that need serialization coverage
+ // AND those types are not already covered by imported modules.
+ var requiredTypes = new HashSet(StringComparer.Ordinal);
+
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is MessagingHandlerInfo { Diagnostics.Count: 0 } handler)
+ {
+ requiredTypes.Add(handler.MessageTypeName);
+
+ if (handler.ResponseTypeName is not null)
+ {
+ requiredTypes.Add(handler.ResponseTypeName);
+ }
+ }
+ else if (syntaxInfo is SagaInfo { Diagnostics.Count: 0 } saga)
+ {
+ requiredTypes.Add(saga.StateTypeName);
+ }
+ }
+
+ // If all required types are covered by imported modules, no local JsonContext is needed.
+ if (requiredTypes.Count == 0 || requiredTypes.IsSubsetOf(importedTypes))
+ {
+ return;
+ }
+
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.MissingJsonSerializerContext,
+ Location.None,
+ moduleInfo.ModuleName));
+
+ return;
+ }
+
+ // Build the set of covered types from the local JsonContext
+ // plus types imported from referenced modules.
+ var coveredTypes = new HashSet(importedTypes, StringComparer.Ordinal);
+
+ foreach (var serializableType in jsonContextInfo.SerializableTypes)
+ {
+ coveredTypes.Add(serializableType.TypeName);
+ }
+
+ // Collect all required message types.
+ var requiredMessageTypes = new HashSet(StringComparer.Ordinal);
+
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is MessagingHandlerInfo { Diagnostics.Count: 0 } handler)
+ {
+ requiredMessageTypes.Add(handler.MessageTypeName);
+
+ if (handler.ResponseTypeName is not null)
+ {
+ requiredMessageTypes.Add(handler.ResponseTypeName);
+ }
+ }
+ else if (syntaxInfo is SagaInfo { Diagnostics.Count: 0 } saga)
+ {
+ requiredMessageTypes.Add(saga.StateTypeName);
+ }
+ }
+
+ // Emit MO0016 for each required type not covered by the JsonSerializerContext
+ // or imported modules.
+ foreach (var requiredType in requiredMessageTypes.OrderBy(t => t, StringComparer.Ordinal))
+ {
+ if (!coveredTypes.Contains(requiredType))
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.MissingJsonSerializable,
+ Location.None,
+ requiredType,
+ jsonContextInfo.JsonContextTypeName));
+ }
+ }
+ }
+
+ private static void ValidateCallSiteJsonContext(
+ SourceProductionContext context,
+ ImmutableArray callSiteInfos,
+ MessagingModuleInfo moduleInfo,
+ bool isAotPublish,
+ JsonContextInfo jsonContextInfo)
+ {
+ if ((!isAotPublish && jsonContextInfo.JsonContextTypeName is null) || callSiteInfos.Length == 0)
+ {
+ return;
+ }
+
+ // Build the set of covered types from the local JsonContext
+ // plus types imported from referenced modules.
+ var coveredTypes = new HashSet(StringComparer.Ordinal);
+
+ foreach (var serializableType in jsonContextInfo.SerializableTypes)
+ {
+ coveredTypes.Add(serializableType.TypeName);
+ }
+
+ foreach (var info in callSiteInfos)
+ {
+ if (info is ImportedModuleTypesInfo imported)
+ {
+ foreach (var typeName in imported.ImportedTypeNames)
+ {
+ coveredTypes.Add(typeName);
+ }
+ }
+ }
+
+ // If there are no covered types at all, nothing to validate against.
+ if (coveredTypes.Count == 0)
+ {
+ return;
+ }
+
+ // Determine the context name for the diagnostic message.
+ // If no local context, use a generic description.
+ var contextDisplayName = jsonContextInfo.JsonContextTypeName ?? "any registered module";
+
+ // Emit MO0018 for each call-site type not covered by the JsonSerializerContext
+ // or imported module registrations. Only check messaging call sites — mediator
+ // dispatch is in-process and does not require JSON serialization.
+ foreach (var info in callSiteInfos)
+ {
+ if (info is CallSiteMessageTypeInfo callSite
+ && callSite.Kind is not (CallSiteKind.MediatorSend or CallSiteKind.MediatorQuery or CallSiteKind.MediatorPublish))
+ {
+ if (!coveredTypes.Contains(callSite.MessageTypeName))
+ {
+ var location = ReconstructLocation(callSite.Location);
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.CallSiteTypeNotInJsonContext,
+ location,
+ callSite.MessageTypeName,
+ callSite.Kind.ToString(),
+ contextDisplayName));
+ }
+
+ if (callSite.ResponseTypeName is not null
+ && !coveredTypes.Contains(callSite.ResponseTypeName))
+ {
+ var location = ReconstructLocation(callSite.Location);
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.CallSiteTypeNotInJsonContext,
+ location,
+ callSite.ResponseTypeName,
+ callSite.Kind.ToString(),
+ contextDisplayName));
+ }
+ }
+ }
+ }
+
+ private static ImmutableArray ExtractContextOnlyTypes(
+ ImmutableArray syntaxInfos,
+ MessagingModuleInfo moduleInfo,
+ JsonContextInfo jsonContextInfo)
+ {
+ if (jsonContextInfo.JsonContextTypeName is null || jsonContextInfo.SerializableTypes.Count == 0)
+ {
+ return syntaxInfos;
+ }
+
+ // Collect handler message type names and response type names.
+ var handlerTypeNames = new HashSet(StringComparer.Ordinal);
+
+ foreach (var info in syntaxInfos)
+ {
+ if (info is MessagingHandlerInfo { Diagnostics.Count: 0 } handler)
+ {
+ handlerTypeNames.Add(handler.MessageTypeName);
+
+ if (handler.ResponseTypeName is not null)
+ {
+ handlerTypeNames.Add(handler.ResponseTypeName);
+ }
+ }
+ }
+
+ // Collect saga state type names.
+ var sagaStateTypeNames = new HashSet(StringComparer.Ordinal);
+
+ foreach (var info in syntaxInfos)
+ {
+ if (info is SagaInfo { Diagnostics.Count: 0 } saga)
+ {
+ sagaStateTypeNames.Add(saga.StateTypeName);
+ }
+ }
+
+ // Find context-only types from the pre-extracted JsonContext data.
+ var contextOnlyInfos = new List();
+
+ foreach (var serializableType in jsonContextInfo.SerializableTypes)
+ {
+ // Skip types already covered by handlers or sagas.
+ if (handlerTypeNames.Contains(serializableType.TypeName)
+ || sagaStateTypeNames.Contains(serializableType.TypeName))
+ {
+ continue;
+ }
+
+ contextOnlyInfos.Add(new ContextOnlyMessageInfo(
+ serializableType.TypeName,
+ serializableType.TypeHierarchy,
+ serializableType.AttributeLocation));
+ }
+
+ if (contextOnlyInfos.Count == 0)
+ {
+ return syntaxInfos;
+ }
+
+ return syntaxInfos.AddRange(contextOnlyInfos);
+ }
+
private static readonly Dictionary s_descriptorLookup = new()
{
[Errors.DuplicateRequestHandler.Id] = Errors.DuplicateRequestHandler,
[Errors.OpenGenericMessagingHandler.Id] = Errors.OpenGenericMessagingHandler,
[Errors.AbstractMessagingHandler.Id] = Errors.AbstractMessagingHandler,
- [Errors.SagaMissingParameterlessConstructor.Id] = Errors.SagaMissingParameterlessConstructor
+ [Errors.SagaMissingParameterlessConstructor.Id] = Errors.SagaMissingParameterlessConstructor,
+ [Errors.MissingJsonSerializerContext.Id] = Errors.MissingJsonSerializerContext,
+ [Errors.MissingJsonSerializable.Id] = Errors.MissingJsonSerializable,
+ [Errors.CallSiteTypeNotInJsonContext.Id] = Errors.CallSiteTypeNotInJsonContext,
+ [Errors.CallSiteNoHandler.Id] = Errors.CallSiteNoHandler
};
private static Diagnostic ReconstructDiagnostic(DiagnosticInfo info)
diff --git a/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj b/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj
index 89d24a499ab..cd0d497e7db 100644
--- a/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj
+++ b/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj
@@ -20,5 +20,6 @@
+
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/CallSiteKind.cs b/src/Mocha/src/Mocha.Analyzers/Models/CallSiteKind.cs
new file mode 100644
index 00000000000..c6ae51cc476
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/CallSiteKind.cs
@@ -0,0 +1,47 @@
+namespace Mocha.Analyzers;
+
+///
+/// Represents the kind of call site detected for a message dispatch invocation.
+///
+public enum CallSiteKind
+{
+ ///
+ /// A PublishAsync<T> call on IMessageBus.
+ ///
+ Publish,
+
+ ///
+ /// A SendAsync<T> call on IMessageBus.
+ ///
+ Send,
+
+ ///
+ /// A SchedulePublishAsync<T> call on IMessageBus.
+ ///
+ SchedulePublish,
+
+ ///
+ /// A ScheduleSendAsync<T> call on IMessageBus.
+ ///
+ ScheduleSend,
+
+ ///
+ /// A RequestAsync<T> call on IMessageBus.
+ ///
+ Request,
+
+ ///
+ /// A SendAsync call on Mocha.Mediator.ISender.
+ ///
+ MediatorSend,
+
+ ///
+ /// A QueryAsync call on Mocha.Mediator.ISender.
+ ///
+ MediatorQuery,
+
+ ///
+ /// A PublishAsync<T> call on Mocha.Mediator.IPublisher.
+ ///
+ MediatorPublish
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/CallSiteMessageTypeInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/CallSiteMessageTypeInfo.cs
new file mode 100644
index 00000000000..777087d05fe
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/CallSiteMessageTypeInfo.cs
@@ -0,0 +1,24 @@
+namespace Mocha.Analyzers;
+
+///
+/// Represents a message type discovered at a call site (method invocation) during source generation.
+/// Call-site infos produce diagnostics only and do not generate code.
+///
+/// The fully qualified type name of the discovered message type.
+/// The kind of call site where the message type was discovered.
+///
+/// The equatable source location of the invocation expression, or if unavailable.
+///
+///
+/// The fully qualified type name of the response type for request-reply call sites, or
+/// for fire-and-forget calls.
+///
+public sealed record CallSiteMessageTypeInfo(
+ string MessageTypeName,
+ CallSiteKind Kind,
+ LocationInfo? Location,
+ string? ResponseTypeName = null) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => $"CallSite:{Kind}:{MessageTypeName}";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/ContextOnlyMessageInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/ContextOnlyMessageInfo.cs
new file mode 100644
index 00000000000..835f93a1aa7
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/ContextOnlyMessageInfo.cs
@@ -0,0 +1,25 @@
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers;
+
+///
+/// Represents a message type discovered from the JsonSerializerContext that does not
+/// have a corresponding handler in the current assembly. These types still need serializer
+/// registrations for AOT compatibility.
+///
+/// The fully qualified type name of the message.
+///
+/// The unfiltered type hierarchy of the message type (base types excluding object, plus all interfaces),
+/// as fully qualified display strings. Filtering to registered types happens in the generator phase.
+///
+///
+/// The equatable source location from the [JsonSerializable] attribute, or if unavailable.
+///
+public sealed record ContextOnlyMessageInfo(
+ string MessageTypeName,
+ ImmutableEquatableArray MessageTypeHierarchy,
+ LocationInfo? Location) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => $"MsgContextOnly:{MessageTypeName}";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/ImportedMediatorModuleTypesInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/ImportedMediatorModuleTypesInfo.cs
new file mode 100644
index 00000000000..7de312239c1
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/ImportedMediatorModuleTypesInfo.cs
@@ -0,0 +1,24 @@
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers;
+
+///
+/// Represents the set of types imported from a referenced module's
+/// [MediatorModuleInfo] attribute. Each instance corresponds to a single
+/// invocation of a module registration method (e.g. builder.AddOrderService()).
+///
+/// The name of the invoked module registration method.
+///
+/// The fully qualified type names listed in the MessageTypes property of the attribute.
+///
+///
+/// The fully qualified type names listed in the HandlerTypes property of the attribute.
+///
+public sealed record ImportedMediatorModuleTypesInfo(
+ string MethodName,
+ ImmutableEquatableArray ImportedTypeNames,
+ ImmutableEquatableArray ImportedHandlerTypeNames) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => $"ImportedMediatorModule:{MethodName}";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/ImportedModuleTypesInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/ImportedModuleTypesInfo.cs
new file mode 100644
index 00000000000..78aec3f4d60
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/ImportedModuleTypesInfo.cs
@@ -0,0 +1,28 @@
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers;
+
+///
+/// Represents the set of types imported from a referenced module's
+/// [MessagingModuleInfo] attribute. Each instance corresponds to a single
+/// invocation of a module registration method (e.g. builder.AddOrderService()).
+///
+/// The name of the invoked module registration method.
+///
+/// The fully qualified type names listed in the MessageTypes property of the attribute.
+///
+///
+/// The fully qualified type names listed in the SagaTypes property of the attribute.
+///
+///
+/// The fully qualified type names listed in the HandlerTypes property of the attribute.
+///
+public sealed record ImportedModuleTypesInfo(
+ string MethodName,
+ ImmutableEquatableArray ImportedTypeNames,
+ ImmutableEquatableArray ImportedSagaTypeNames,
+ ImmutableEquatableArray ImportedHandlerTypeNames) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => $"ImportedModule:{MethodName}";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/JsonContextInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/JsonContextInfo.cs
new file mode 100644
index 00000000000..57e19bd7f75
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/JsonContextInfo.cs
@@ -0,0 +1,20 @@
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers;
+
+///
+/// Pre-extracted, equatable information about the JsonSerializerContext referenced
+/// from a [MessagingModule] attribute. This model flows through the incremental
+/// pipeline in place of to avoid
+/// re-execution on every keystroke.
+///
+///
+/// The fully qualified type name of the JsonSerializerContext, or if not specified.
+///
+///
+/// The types declared via [JsonSerializable(typeof(T))] on the context, including their
+/// type hierarchies for context-only message resolution.
+///
+public sealed record JsonContextInfo(
+ string? JsonContextTypeName,
+ ImmutableEquatableArray SerializableTypes);
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/JsonContextSerializableTypesInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/JsonContextSerializableTypesInfo.cs
new file mode 100644
index 00000000000..cd6b9c150c6
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/JsonContextSerializableTypesInfo.cs
@@ -0,0 +1,20 @@
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers;
+
+///
+/// Carries the complete set of type names declared as [JsonSerializable] on the
+/// module's JsonSerializerContext. Used by downstream generators to determine which
+/// types have local serializer support and should receive AddMessageConfiguration
+/// registrations with pre-built serializers.
+///
+///
+/// The fully qualified type names of all types declared via [JsonSerializable(typeof(T))]
+/// on the module's JsonSerializerContext.
+///
+public sealed record JsonContextSerializableTypesInfo(
+ ImmutableEquatableArray TypeNames) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => "JsonContextTypes";
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/JsonSerializableTypeInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/JsonSerializableTypeInfo.cs
new file mode 100644
index 00000000000..bf696fd0cd8
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/JsonSerializableTypeInfo.cs
@@ -0,0 +1,20 @@
+using Mocha.Analyzers.Utils;
+
+namespace Mocha.Analyzers;
+
+///
+/// Represents a type discovered from a [JsonSerializable(typeof(T))] attribute on a
+/// JsonSerializerContext, including its type hierarchy for context-only message resolution.
+///
+/// The fully qualified type name of the serializable type.
+///
+/// The unfiltered type hierarchy (base types excluding object, plus all interfaces),
+/// as fully qualified display strings.
+///
+///
+/// The equatable source location of the [JsonSerializable] attribute, or if unavailable.
+///
+public sealed record JsonSerializableTypeInfo(
+ string TypeName,
+ ImmutableEquatableArray TypeHierarchy,
+ LocationInfo? AttributeLocation) : IEquatable;
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/MessagingHandlerInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/MessagingHandlerInfo.cs
index 5cccda6c3a6..534b59a0a82 100644
--- a/src/Mocha/src/Mocha.Analyzers/Models/MessagingHandlerInfo.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Models/MessagingHandlerInfo.cs
@@ -1,3 +1,5 @@
+using Mocha.Analyzers.Utils;
+
namespace Mocha.Analyzers;
///
@@ -10,6 +12,10 @@ namespace Mocha.Analyzers;
/// The fully qualified type name of the response, or if the handler returns no response.
///
/// The kind of messaging handler.
+///
+/// The unfiltered type hierarchy of the message type (base types excluding object, plus all interfaces),
+/// as fully qualified display strings. Filtering to registered types happens in the generator phase.
+///
///
/// The equatable source location of the handler type declaration, or if unavailable.
///
@@ -19,6 +25,7 @@ public sealed record MessagingHandlerInfo(
string MessageTypeName,
string? ResponseTypeName,
MessagingHandlerKind Kind,
+ ImmutableEquatableArray MessageTypeHierarchy,
LocationInfo? Location) : SyntaxInfo
{
///
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/MessagingModuleInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/MessagingModuleInfo.cs
index 3eeaea2029d..dea7126555c 100644
--- a/src/Mocha/src/Mocha.Analyzers/Models/MessagingModuleInfo.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Models/MessagingModuleInfo.cs
@@ -5,7 +5,11 @@ namespace Mocha.Analyzers;
/// discovered during source generation.
///
/// The module name specified in the attribute.
-public sealed record MessagingModuleInfo(string ModuleName) : SyntaxInfo
+///
+/// The fully qualified type name of the JsonSerializerContext specified via
+/// the JsonContext named property, or null if not specified.
+///
+public sealed record MessagingModuleInfo(string ModuleName, string? JsonContextTypeName = null) : SyntaxInfo
{
///
public override string OrderByKey => $"MsgModule:{ModuleName}";
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/SagaInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/SagaInfo.cs
index 8e776c96a4b..3b82e9e9a84 100644
--- a/src/Mocha/src/Mocha.Analyzers/Models/SagaInfo.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Models/SagaInfo.cs
@@ -5,9 +5,11 @@ namespace Mocha.Analyzers;
///
/// The fully qualified type name of the saga class.
/// The namespace containing the saga class.
+/// The fully qualified type name of the saga state type (TState).
public sealed record SagaInfo(
string SagaTypeName,
- string SagaNamespace) : SyntaxInfo
+ string SagaNamespace,
+ string StateTypeName) : SyntaxInfo
{
///
public override string OrderByKey => $"Saga:{SagaTypeName}";
diff --git a/src/Mocha/src/Mocha.Analyzers/README.md b/src/Mocha/src/Mocha.Analyzers/README.md
new file mode 100644
index 00000000000..b7580a4defc
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/README.md
@@ -0,0 +1,308 @@
+# Mocha Messaging Source Generator
+
+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.
+
+## Table of Contents
+
+- [Module Declaration](#module-declaration)
+- [Discovery Pipeline](#discovery-pipeline)
+- [Handler Discovery](#handler-discovery)
+- [Saga Discovery](#saga-discovery)
+- [Call-Site Discovery](#call-site-discovery)
+- [Imported Module Discovery](#imported-module-discovery)
+- [Code Generation](#code-generation)
+- [AOT Mode](#aot-mode)
+- [Validation & Diagnostics](#validation--diagnostics)
+- [Cross-Module System](#cross-module-system)
+
+---
+
+## Module Declaration
+
+A module is declared via an assembly-level attribute:
+
+```csharp
+[assembly: MessagingModule("OrderService")]
+```
+
+With AOT support:
+
+```csharp
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+```
+
+- **Module name** is required and determines the generated method name (`Add{ModuleName}`).
+- **JsonContext** is optional. When set, the generator enters AOT mode and emits pre-built serializer registrations.
+- Only one `[MessagingModule]` per assembly is supported.
+
+---
+
+## Discovery Pipeline
+
+The generator runs two parallel incremental pipelines:
+
+### Pipeline 1: Handler & Saga Discovery
+
+Syntax predicate filters classes/records with Mocha base types in their base list, then inspectors run in priority order:
+
+1. **MessagingHandlerInspector** — concrete handlers implementing messaging interfaces
+2. **AbstractMessagingHandlerInspector** — abstract handlers (diagnostic-only, MO0013)
+3. **MessagingModuleInspector** — `[assembly: MessagingModule(...)]` attributes
+4. **SagaInspector** — `Saga` subclasses
+
+### Pipeline 2: Call-Site & Import Discovery
+
+Syntax predicate filters invocation expressions, then:
+
+1. **CallSiteMessageTypeInspector** — dispatch calls on `IMessageBus`, `ISender`, `IPublisher`
+2. **ImportedModuleTypeInspector** — calls to methods annotated with `[MessagingModuleInfo]`
+
+Both pipelines feed into the `Execute` method which validates, augments, and generates code.
+
+---
+
+## Handler Discovery
+
+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` | Batch | No |
+| 2 | `IConsumer` | Consumer | No |
+| 3 | `IEventRequestHandler` | RequestResponse | Yes |
+| 4 | `IEventRequestHandler` | Send | No |
+| 5 | `IEventHandler` | 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.
+
+---
+
+## Saga Discovery
+
+Classes (not records) that inherit from `Saga` are discovered. The inspector:
+
+1. Walks the base type chain to find `Saga` and extracts `TState`.
+2. Validates that the saga has a **public parameterless constructor** (required for instantiation). Reports MO0014 if missing.
+
+---
+
+## Call-Site Discovery
+
+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).
+
+### IMessageBus Methods
+
+| Method | CallSiteKind | Type Extraction |
+|--------|-------------|-----------------|
+| `PublishAsync` | Publish | Type argument |
+| `SendAsync` | Send | Type argument |
+| `SchedulePublishAsync` | SchedulePublish | Type argument |
+| `ScheduleSendAsync` | ScheduleSend | Type argument |
+| `RequestAsync` | Request | First argument type + type argument for response |
+| `RequestAsync` (non-generic) | Request | First argument type (fallback) |
+
+### ISender Methods (Mediator)
+
+| Method | CallSiteKind | Type Extraction |
+|--------|-------------|-----------------|
+| `SendAsync` | MediatorSend | First argument type |
+| `QueryAsync` | MediatorQuery | First argument type |
+
+### IPublisher Methods (Mediator)
+
+| Method | CallSiteKind | Type Extraction |
+|--------|-------------|-----------------|
+| `PublishAsync` | MediatorPublish | Type argument |
+
+**Mediator call sites are excluded from JSON validation** because mediator dispatch is in-process and does not require serialization.
+
+---
+
+## Imported Module Discovery
+
+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:
+
+- Excluded from local serializer registration (no duplicate `AddMessageConfiguration`)
+- Counted as "covered" in AOT validation (MO0015, MO0016, MO0018)
+
+---
+
+## Code Generation
+
+The generator emits a single extension method on `IMessageBusHostBuilder`:
+
+```csharp
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class {Module}MessageBusBuilderExtensions
+ {
+ [MessagingModuleInfo(MessageTypes = new Type[] { ... })]
+ public static IMessageBusHostBuilder Add{Module}(
+ this IMessageBusHostBuilder builder)
+ {
+ // registrations
+ return builder;
+ }
+ }
+}
+```
+
+### Registration Order
+
+Registrations are emitted in this order:
+
+1. **AOT Configuration** (if JsonContext specified)
+ - `ModifyOptions(builder, o => o.IsAotCompatible = true)`
+ - `AddJsonTypeInfoResolver(builder, {JsonContext}.Default)`
+2. **Message Type Serializers** — `AddMessageConfiguration` per type
+3. **Saga Configuration** — `AddSagaConfiguration` with state serializer
+4. **Batch Handlers** — sorted by handler type name
+5. **Consumers** — sorted by handler type name
+6. **Request Handlers** (RequestResponse + Send) — sorted by handler type name
+7. **Event Handlers** — sorted by handler type name
+8. **Saga Registrations** — `AddSaga`
+
+### Handler Registration
+
+Each handler emits `AddHandlerConfiguration` with a factory:
+
+| Kind | Factory |
+|------|---------|
+| Event | `ConsumerFactory.Subscribe()` |
+| Send | `ConsumerFactory.Send()` |
+| RequestResponse | `ConsumerFactory.Request()` |
+| Consumer | `ConsumerFactory.Consume()` |
+| Batch | `ConsumerFactory.Batch()` |
+
+### `[MessagingModuleInfo]` Attribute Population
+
+The `MessageTypes` array on the generated method contains **only types that receive `AddMessageConfiguration` calls** in the method body. This means:
+
+- Only types present in the **local** `JsonSerializerContext` (not from referenced assemblies)
+- Excluding types already covered by imported modules
+- Including context-only types (types in the JsonContext without handlers)
+- **Empty** when no JsonContext is specified (non-AOT mode)
+
+This ensures importing modules know exactly which types have serializer registrations from this module.
+
+---
+
+## AOT Mode
+
+AOT mode has two independent aspects controlled by different settings:
+
+- **`JsonContext` on `[MessagingModule]`** — controls **code generation**: serializer registrations, strict mode, and resolver registration are only emitted when a `JsonContext` is specified.
+- **`PublishAot` MSBuild property** — controls **validation strictness**: when `true`, diagnostics MO0015/MO0016/MO0018 fire even without a local `JsonContext`.
+
+Validation diagnostics fire when **either** condition is true. Serializer code generation requires `JsonContext`.
+
+### What changes when JsonContext is specified
+
+1. **Serializer registrations are emitted** — `AddMessageConfiguration` with pre-built `JsonMessageSerializer` for each message type in the local JsonContext.
+
+2. **Strict mode is enabled** — `IsAotCompatible = true` on builder options.
+
+3. **JsonTypeInfoResolver is registered** — the specified `JsonSerializerContext` is added as a resolver.
+
+4. **Validation diagnostics fire** — MO0015, MO0016, MO0018 are checked.
+
+### Which types get serializer registrations
+
+A type gets an `AddMessageConfiguration` call if **all** of these are true:
+- It is declared as `[JsonSerializable(typeof(T))]` on the **local** `JsonSerializerContext`
+- It is NOT already imported from a referenced module
+- It is either a handler message/response type OR a context-only type
+
+### Context-Only Types
+
+Types 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.
+
+### Enclosed Types
+
+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.
+
+---
+
+## Validation & Diagnostics
+
+### MO0011 — Duplicate Request Handler (Error)
+
+**Fires when:** Multiple handlers exist for the same message type with `Send` or `RequestResponse` kind.
+
+**Example:** Two handlers both implement `IEventRequestHandler`.
+
+### MO0012 — Open Generic Handler (Info)
+
+**Fires when:** A handler class has unbound type parameters (e.g., `class MyHandler : IEventHandler`).
+
+**Reason:** The generator cannot register open generic handlers — concrete types are required.
+
+### MO0013 — Abstract Handler (Warning)
+
+**Fires when:** An abstract class implements a messaging interface.
+
+**Reason:** Abstract classes cannot be instantiated and thus cannot be registered as handlers.
+
+### MO0014 — Saga Missing Parameterless Constructor (Error)
+
+**Fires when:** A `Saga` subclass does not have a `public` parameterless constructor.
+
+**Reason:** The saga runtime requires `new()` to instantiate saga instances.
+
+### MO0015 — Missing JsonSerializerContext (Error)
+
+**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.
+
+### MO0016 — Missing JsonSerializable (Error)
+
+**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.
+
+### MO0018 — Call-Site Type Not in JsonContext (Warning)
+
+**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.
+
+### When diagnostics fire
+
+| 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` OR `JsonContext` is specified |
+| MO0016 | `PublishAot == true` OR `JsonContext` is specified |
+| MO0018 | `PublishAot == true` OR `JsonContext` is specified |
+
+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.
+
+---
+
+## Cross-Module System
+
+The module system enables multiple assemblies to register their handlers independently while avoiding duplicate serializer registrations.
+
+### How it works
+
+1. **Module A** declares `[assembly: MessagingModule("Orders", JsonContext = typeof(OrdersJsonContext))]`
+2. The generator emits `AddOrders()` with `[MessagingModuleInfo(MessageTypes = new[] { typeof(OrderCreated), ... })]`
+3. **Module B** calls `builder.AddOrders()` in its code
+4. The `ImportedModuleTypeInspector` reads the `[MessagingModuleInfo]` attribute and extracts the type names
+5. Module B's generator skips serializer registration for imported types and excludes them from its own `[MessagingModuleInfo]`
+
+### Validation with imports
+
+- **MO0015:** If all handler types are covered by imports, no local JsonContext is needed
+- **MO0016:** Imported types are considered "covered" — no local `[JsonSerializable]` needed
+- **MO0018:** Imported types are considered "covered" at call sites
+
+### Key constraint
+
+The `[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.
diff --git a/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs b/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs
index 0b9355f01c3..06df5cb5232 100644
--- a/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs
+++ b/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs
@@ -95,4 +95,53 @@ public static class SyntaxConstants
/// Gets the metadata name for the MessagingModuleAttribute class.
///
public const string MessagingModuleAttribute = "Mocha.MessagingModuleAttribute";
+
+ ///
+ /// Gets the named property name for the JsonContext property
+ /// on MessagingModuleAttribute.
+ ///
+ public const string JsonContextProperty = "JsonContext";
+
+ ///
+ /// Gets the metadata name for the MessagingModuleInfoAttribute class.
+ ///
+ public const string MessagingModuleInfoAttribute = "Mocha.MessagingModuleInfoAttribute";
+
+ ///
+ /// Gets the metadata name for the MediatorModuleInfoAttribute class.
+ ///
+ public const string MediatorModuleInfoAttribute = "Mocha.Mediator.MediatorModuleInfoAttribute";
+
+ ///
+ /// Gets the named property name for the MessageTypes property
+ /// on MessagingModuleInfoAttribute and MediatorModuleInfoAttribute.
+ ///
+ public const string MessageTypesProperty = "MessageTypes";
+
+ ///
+ /// Gets the named property name for the SagaTypes property
+ /// on MessagingModuleInfoAttribute.
+ ///
+ public const string SagaTypesProperty = "SagaTypes";
+
+ ///
+ /// Gets the named property name for the HandlerTypes property
+ /// on MessagingModuleInfoAttribute and MediatorModuleInfoAttribute.
+ ///
+ public const string HandlerTypesProperty = "HandlerTypes";
+
+ ///
+ /// Gets the metadata name for the IMessageBus interface.
+ ///
+ public const string IMessageBus = "Mocha.IMessageBus";
+
+ ///
+ /// Gets the metadata name for the ISender interface (Mediator).
+ ///
+ public const string ISender = "Mocha.Mediator.ISender";
+
+ ///
+ /// Gets the metadata name for the IPublisher interface (Mediator).
+ ///
+ public const string IPublisher = "Mocha.Mediator.IPublisher";
}
diff --git a/src/Mocha/src/Mocha.Analyzers/buildTransitive/Mocha.Analyzers.props b/src/Mocha/src/Mocha.Analyzers/buildTransitive/Mocha.Analyzers.props
new file mode 100644
index 00000000000..05c7c3b4610
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/buildTransitive/Mocha.Analyzers.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleInfoAttribute.cs b/src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleInfoAttribute.cs
new file mode 100644
index 00000000000..81f3b6c9103
--- /dev/null
+++ b/src/Mocha/src/Mocha.Mediator.Abstractions/MediatorModuleInfoAttribute.cs
@@ -0,0 +1,20 @@
+namespace Mocha.Mediator;
+
+///
+/// Annotates a generated mediator registration method with metadata about the message types
+/// it registers. This enables cross-project analyzers to discover types registered by
+/// referenced modules without scanning assemblies.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+public sealed class MediatorModuleInfoAttribute : Attribute
+{
+ ///
+ /// Gets or sets the message types registered by this module.
+ ///
+ public Type[] MessageTypes { get; set; } = [];
+
+ ///
+ /// Gets or sets the handler types registered by this module.
+ ///
+ public Type[] HandlerTypes { get; set; } = [];
+}
diff --git a/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj b/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj
index 351ef6467a7..48f0b994904 100644
--- a/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj
+++ b/src/Mocha/src/Mocha.Mediator.Abstractions/Mocha.Mediator.Abstractions.csproj
@@ -4,5 +4,6 @@
Mocha.Mediator
enable
enable
+ true
diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs
index 59144942e17..5d9fd39d446 100644
--- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs
+++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -15,22 +16,7 @@ namespace Mocha.Mediator;
///
public sealed class MediatorBuilder : IMediatorBuilder
{
- private static readonly MethodInfo s_buildCommandPipeline =
- typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildCommandPipeline))!;
-
- private static readonly MethodInfo s_buildCommandResponsePipeline =
- typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildCommandResponsePipeline))!;
-
- private static readonly MethodInfo s_buildQueryPipeline =
- typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildQueryPipeline))!;
-
- private static readonly MethodInfo s_buildNotificationPipeline =
- typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildNotificationPipeline))!;
-
- private readonly List _middlewares =
- [
- MediatorDiagnosticMiddleware.Create()
- ];
+ private readonly List _middlewares = [MediatorDiagnosticMiddleware.Create()];
private readonly List>> _pipelineModifiers = [];
private readonly Dictionary> _handlerDescriptors = [];
@@ -106,8 +92,7 @@ public IMediatorBuilder ConfigureServices(Action
- public void AddHandler(Action? configure = null)
- where THandler : class
+ public void AddHandler(Action? configure = null) where THandler : class
{
var handlerType = typeof(THandler);
var existing = _handlerDescriptors.GetValueOrDefault(handlerType);
@@ -172,6 +157,14 @@ void ApplyConfig(MediatorHandlerDescriptor d)
///
/// The application-level service provider used to resolve shared services.
///
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026:RequiresUnreferencedCode",
+ Justification = "Reflection fallback only used for manual (non-generated) handler registration.")]
+ [UnconditionalSuppressMessage(
+ "AOT",
+ "IL3050:RequiresDynamicCode",
+ Justification = "Reflection fallback only used for manual (non-generated) handler registration.")]
public MediatorRuntime Build(IServiceProvider applicationServices)
{
// Create the mediator's own internal service collection.
@@ -202,13 +195,13 @@ public MediatorRuntime Build(IServiceProvider applicationServices)
// Compile pipelines using the internal provider for middleware factory context.
var factoryCtx = new MediatorMiddlewareFactoryContext { Services = internalProvider, Features = features };
- var middlewareConfigs = _middlewares.Count > 0
- ? new IReadOnlyList[] { _middlewares }
- : [];
+ var middlewareConfigs =
+ _middlewares.Count > 0 ? new IReadOnlyList[] { _middlewares } : [];
- var modifiers = _pipelineModifiers.Count > 0
- ? new IReadOnlyList>>[] { _pipelineModifiers }
- : [];
+ var modifiers =
+ _pipelineModifiers.Count > 0
+ ? new IReadOnlyList>>[] { _pipelineModifiers }
+ : [];
var pipelines = new Dictionary();
var notificationTerminals = new Dictionary>();
@@ -236,8 +229,11 @@ public MediatorRuntime Build(IServiceProvider applicationServices)
factoryCtx.MessageType = config.MessageType!;
factoryCtx.ResponseType = config.ResponseType;
- pipelines[config.MessageType!] =
- MediatorMiddlewareCompiler.Compile(factoryCtx, terminal, middlewareConfigs, modifiers);
+ pipelines[config.MessageType!] = MediatorMiddlewareCompiler.Compile(
+ factoryCtx,
+ terminal,
+ middlewareConfigs,
+ modifiers);
}
}
@@ -253,8 +249,8 @@ public MediatorRuntime Build(IServiceProvider applicationServices)
var compiled = ImmutableArray.CreateBuilder(terminals.Count);
for (var i = 0; i < terminals.Count; i++)
{
- compiled.Add(MediatorMiddlewareCompiler.Compile(
- factoryCtx, terminals[i], middlewareConfigs, modifiers));
+ compiled.Add(
+ MediatorMiddlewareCompiler.Compile(factoryCtx, terminals[i], middlewareConfigs, modifiers));
}
notificationPipelines[notificationType] = compiled.ToImmutable();
@@ -270,27 +266,37 @@ public MediatorRuntime Build(IServiceProvider applicationServices)
_options.NotificationPublishMode);
}
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
private static MediatorDelegate BuildPipelineViaReflection(MediatorHandlerConfiguration config)
{
+ var buildCommandPipeline = typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildCommandPipeline))!;
+
+ var buildCommandResponsePipeline = typeof(PipelineBuilder).GetMethod(
+ nameof(PipelineBuilder.BuildCommandResponsePipeline))!;
+
+ var buildQueryPipeline = typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildQueryPipeline))!;
+
+ var buildNotificationPipeline = typeof(PipelineBuilder).GetMethod(
+ nameof(PipelineBuilder.BuildNotificationPipeline))!;
+
return config.Kind switch
{
- MediatorHandlerKind.Command =>
- (MediatorDelegate)s_buildCommandPipeline
- .MakeGenericMethod(config.HandlerType!, config.MessageType!)
- .Invoke(null, null)!,
+ MediatorHandlerKind.Command => (MediatorDelegate)
+ buildCommandPipeline.MakeGenericMethod(config.HandlerType!, config.MessageType!).Invoke(null, null)!,
- MediatorHandlerKind.CommandResponse =>
- (MediatorDelegate)s_buildCommandResponsePipeline
+ MediatorHandlerKind.CommandResponse => (MediatorDelegate)
+ buildCommandResponsePipeline
.MakeGenericMethod(config.HandlerType!, config.MessageType!, config.ResponseType!)
.Invoke(null, null)!,
- MediatorHandlerKind.Query =>
- (MediatorDelegate)s_buildQueryPipeline
+ MediatorHandlerKind.Query => (MediatorDelegate)
+ buildQueryPipeline
.MakeGenericMethod(config.HandlerType!, config.MessageType!, config.ResponseType!)
.Invoke(null, null)!,
- MediatorHandlerKind.Notification =>
- (MediatorDelegate)s_buildNotificationPipeline
+ MediatorHandlerKind.Notification => (MediatorDelegate)
+ buildNotificationPipeline
.MakeGenericMethod(config.HandlerType!, config.MessageType!)
.Invoke(null, null)!,
diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs
index 153e63820c0..a8491180e26 100644
--- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs
+++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilderInstrumentationExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -24,7 +25,9 @@ public static IMediatorBuilder AddInstrumentation(this IMediatorBuilder builder)
///
/// Registers a custom implementation.
///
- public static IMediatorBuilder AddDiagnosticEventListener(this IMediatorBuilder builder)
+ public static IMediatorBuilder AddDiagnosticEventListener<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
+ this IMediatorBuilder builder)
where T : class, IMediatorDiagnosticEventListener
{
ArgumentNullException.ThrowIfNull(builder);
diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs
index 46037ccd531..5ab56f2371f 100644
--- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs
+++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace Mocha.Mediator;
@@ -53,7 +54,9 @@ public static IMediatorHostBuilder AddInstrumentation(this IMediatorHostBuilder
/// Registers a custom implementation
/// into the mediator's internal services.
///
- public static IMediatorHostBuilder AddDiagnosticEventListener(this IMediatorHostBuilder builder)
+ public static IMediatorHostBuilder AddDiagnosticEventListener<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
+ this IMediatorHostBuilder builder)
where T : class, IMediatorDiagnosticEventListener
{
ArgumentNullException.ThrowIfNull(builder);
diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs
index 5fea8ccd6b9..994e952f1f8 100644
--- a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs
+++ b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorHostBuilderHandlerExtensions.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -20,6 +21,8 @@ public static class MediatorHostBuilderHandlerExtensions
/// The handler implementation type.
/// The mediator host builder.
/// An optional action to configure the handler descriptor.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
public static IMediatorHostBuilder AddHandler(
this IMediatorHostBuilder builder,
Action? configure = null)
@@ -43,7 +46,8 @@ public static IMediatorHostBuilder AddHandler(
/// The mediator host builder.
/// The pre-built handler configuration.
[EditorBrowsable(EditorBrowsableState.Never)]
- public static IMediatorHostBuilder AddHandlerConfiguration(
+ public static IMediatorHostBuilder AddHandlerConfiguration<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
this IMediatorHostBuilder builder,
MediatorHandlerConfiguration configuration)
where THandler : class
diff --git a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs
index 1706e688ff4..962257c45ab 100644
--- a/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs
+++ b/src/Mocha/src/Mocha.Mediator/Descriptors/MediatorHandlerDescriptor.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Mocha.Mediator;
///
@@ -14,8 +16,7 @@ public class MediatorHandlerDescriptor
///
/// The mediator configuration context.
/// The concrete handler implementation type.
- public MediatorHandlerDescriptor(IMediatorConfigurationContext context, Type handlerType)
- : base(context)
+ public MediatorHandlerDescriptor(IMediatorConfigurationContext context, Type handlerType) : base(context)
{
ArgumentNullException.ThrowIfNull(handlerType);
@@ -31,6 +32,10 @@ public MediatorHandlerDescriptor(IMediatorConfigurationContext context, Type han
/// , auto-detects handler metadata
/// from the handler type's interfaces.
///
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026:RequiresUnreferencedCode",
+ Justification = "Reflection fallback only used for manual (non-generated) handler registration.")]
public MediatorHandlerConfiguration CreateConfiguration()
{
if (Configuration.MessageType is null)
@@ -41,6 +46,8 @@ public MediatorHandlerConfiguration CreateConfiguration()
return Configuration;
}
+ [RequiresUnreferencedCode(
+ "Handler detection uses reflection. Use source-generated AddHandlerConfiguration for AOT compatibility.")]
private void DetectHandler(Type handlerType)
{
var found = false;
diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs
index 0605707f264..78761273cec 100644
--- a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs
+++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Mocha.Mediator;
///
@@ -58,6 +60,7 @@ public static bool IsResponseAssignableTo(this MediatorMiddlewareFactoryConte
public static bool IsResponseAssignableTo(this MediatorMiddlewareFactoryContext context, Type type)
=> context.ResponseType is not null && type.IsAssignableFrom(context.ResponseType);
+ [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Metadata read on statically-referenced types is AOT-safe.")]
private static bool HasGenericInterface(Type type, Type openGeneric)
{
foreach (var @interface in type.GetInterfaces())
diff --git a/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj b/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj
index 85557408cce..bbbc6216716 100644
--- a/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj
+++ b/src/Mocha/src/Mocha.Mediator/Mocha.Mediator.csproj
@@ -4,6 +4,7 @@
Mocha.Mediator
enable
enable
+ true
diff --git a/src/Mocha/src/Mocha/Abstractions/IMessageBusBuilder.cs b/src/Mocha/src/Mocha/Abstractions/IMessageBusBuilder.cs
index d6ef05eb24a..06c551b34bc 100644
--- a/src/Mocha/src/Mocha/Abstractions/IMessageBusBuilder.cs
+++ b/src/Mocha/src/Mocha/Abstractions/IMessageBusBuilder.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Mocha.Middlewares;
using Mocha.Sagas;
@@ -17,6 +18,8 @@ public interface IMessageBusBuilder
/// The handler type implementing .
/// Optional action to configure the consumer descriptor.
/// The builder instance for method chaining.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
IMessageBusBuilder AddHandler(Action? configure = null)
where THandler : class, IHandler;
@@ -34,6 +37,8 @@ IMessageBusBuilder AddHandler(Action? configure =
/// The batch handler type.
/// Optional action to configure batch options.
/// The builder instance for method chaining.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
IMessageBusBuilder AddBatchHandler(Action? configure = null)
where THandler : class, IBatchEventHandler;
@@ -94,6 +99,23 @@ IMessageBusBuilder AddBatchHandler(Action? configure = n
/// The builder instance for method chaining.
IMessageBusBuilder Host(Action configure);
+ ///
+ /// Registers a message type using pre-built configuration from the source generator.
+ ///
+ /// The pre-built message configuration.
+ /// The builder instance for method chaining.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ IMessageBusBuilder AddMessageConfiguration(MessagingMessageConfiguration configuration);
+
+ ///
+ /// Registers a saga using pre-built configuration from the source generator.
+ ///
+ /// The saga type.
+ /// The pre-built saga configuration.
+ /// The builder instance for method chaining.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ IMessageBusBuilder AddSagaConfiguration(MessagingSagaConfiguration configuration) where TSaga : Saga, new();
+
///
/// Registers a message type with the bus and configures its serialization, routing, and metadata.
///
diff --git a/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs b/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs
index d1bebba0c3c..4b5b0a8b7e8 100644
--- a/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs
+++ b/src/Mocha/src/Mocha/Builder/MessageBusBuilder.cs
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -66,6 +67,8 @@ public IMessageBusBuilder ConfigureFeature(Action configure)
}
///
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
public IMessageBusBuilder AddHandler(Action? configure = null)
where THandler : class, IHandler
{
@@ -83,9 +86,7 @@ public IMessageBusBuilder AddHandler(Action? conf
inner(d);
configure(d);
}
-#pragma warning disable format
- : configure;
-#pragma warning restore format
+ : configure;
}
return this;
@@ -143,12 +144,13 @@ public IMessageBusBuilder AddHandler(Action? conf
throw ThrowHelper.InvalidHandlerType();
}
- _consumerRegistrations.Add(new ConsumerRegistration
- {
- HandlerType = handlerType,
- Configure = configure,
- Factory = factory
- });
+ _consumerRegistrations.Add(
+ new ConsumerRegistration
+ {
+ HandlerType = handlerType,
+ Configure = configure,
+ Factory = factory
+ });
return this;
}
@@ -166,12 +168,13 @@ public IMessageBusBuilder AddHandlerConfiguration(MessagingHandlerConfiguration
return this;
}
- _consumerRegistrations.Add(new ConsumerRegistration
- {
- HandlerType = handlerType,
- Configure = null,
- Factory = configuration.Factory
- });
+ _consumerRegistrations.Add(
+ new ConsumerRegistration
+ {
+ HandlerType = handlerType,
+ Configure = null,
+ Factory = configuration.Factory
+ });
return this;
}
@@ -182,6 +185,8 @@ public IMessageBusBuilder AddHandlerConfiguration(MessagingHandlerConfiguration
/// The batch handler type.
/// Optional action to configure batch options.
/// The builder instance for method chaining.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
public IMessageBusBuilder AddBatchHandler(Action? configure = null)
where THandler : class, IBatchEventHandler
{
@@ -198,12 +203,66 @@ public IMessageBusBuilder AddBatchHandler(Action? config
{
var sagaType = typeof(TSaga);
- if (_sagaRegistrations.Exists(r => r.SagaType == sagaType))
+ _sagaRegistrations.RemoveAll(r => r.SagaType == sagaType);
+
+ _sagaRegistrations.Add(new SagaRegistration { SagaType = sagaType, Factory = static () => new TSaga() });
+
+ return this;
+ }
+
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public IMessageBusBuilder AddMessageConfiguration(MessagingMessageConfiguration configuration)
+ {
+ var messageType = configuration.MessageType;
+ var serializer = configuration.Serializer;
+ var enclosedTypes = configuration.EnclosedTypes;
+
+ var configure = (IMessageTypeDescriptor descriptor) =>
{
- return this;
+ descriptor.AddSerializer(serializer);
+
+ if (enclosedTypes is not null)
+ {
+ descriptor.Extend().Configuration.EnclosedTypes = enclosedTypes;
+ }
+ };
+
+ var existingDelegate = _messageDescriptors.GetValueOrDefault(messageType);
+
+ if (existingDelegate is not null)
+ {
+ var innerDelegate = existingDelegate;
+ _messageDescriptors[messageType] = descriptor =>
+ {
+ innerDelegate(descriptor);
+ configure(descriptor);
+ };
+ }
+ else
+ {
+ _messageDescriptors[messageType] = configure;
}
- _sagaRegistrations.Add(new SagaRegistration { SagaType = sagaType, Factory = static () => new TSaga() });
+ return this;
+ }
+
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public IMessageBusBuilder AddSagaConfiguration(MessagingSagaConfiguration configuration)
+ where TSaga : Saga, new()
+ {
+ var sagaType = typeof(TSaga);
+
+ _sagaRegistrations.RemoveAll(r => r.SagaType == sagaType);
+
+ _sagaRegistrations.Add(
+ new SagaRegistration
+ {
+ SagaType = sagaType,
+ Factory = static () => new TSaga(),
+ StateSerializer = configuration.StateSerializer
+ });
return this;
}
@@ -390,6 +449,12 @@ public MessagingRuntime Build(IServiceProvider applicationServices)
foreach (var reg in _sagaRegistrations)
{
var saga = reg.Factory();
+
+ if (reg.StateSerializer is not null)
+ {
+ saga.StateSerializer = reg.StateSerializer;
+ }
+
sagas.Add(saga);
consumerList.Add(saga.Consumer);
}
@@ -454,6 +519,7 @@ public MessagingRuntime Build(IServiceProvider applicationServices)
configureDelegate(descriptor);
var configuration = descriptor.CreateConfiguration();
+
var messageType = new MessageType();
messageType.Initialize(setupContext, configuration);
messageRegistry.AddMessageType(messageType);
diff --git a/src/Mocha/src/Mocha/Builder/Options/IReadOnlyMessagingOptions.cs b/src/Mocha/src/Mocha/Builder/Options/IReadOnlyMessagingOptions.cs
index 13b32f06a7f..f705856903f 100644
--- a/src/Mocha/src/Mocha/Builder/Options/IReadOnlyMessagingOptions.cs
+++ b/src/Mocha/src/Mocha/Builder/Options/IReadOnlyMessagingOptions.cs
@@ -10,4 +10,10 @@ public interface IReadOnlyMessagingOptions
/// is specified on a message type.
///
MessageContentType DefaultContentType { get; }
+
+ ///
+ /// Gets a value indicating whether all message types must be explicitly registered at startup.
+ /// When true, runtime auto-registration of unknown types is disabled.
+ ///
+ bool IsAotCompatible { get; }
}
diff --git a/src/Mocha/src/Mocha/Builder/Options/MessagingOptions.cs b/src/Mocha/src/Mocha/Builder/Options/MessagingOptions.cs
index 8a129064b25..acb39d452d9 100644
--- a/src/Mocha/src/Mocha/Builder/Options/MessagingOptions.cs
+++ b/src/Mocha/src/Mocha/Builder/Options/MessagingOptions.cs
@@ -10,4 +10,11 @@ public class MessagingOptions : IReadOnlyMessagingOptions
/// .
///
public MessageContentType DefaultContentType { get; set; } = MessageContentType.Json;
+
+ ///
+ /// Gets or sets a value indicating whether all message types must be explicitly registered at
+ /// startup. When true, runtime auto-registration of unknown types throws instead of
+ /// falling back to reflection-based serialization. Defaults to false.
+ ///
+ public bool IsAotCompatible { get; set; }
}
diff --git a/src/Mocha/src/Mocha/Builder/SagaRegistration.cs b/src/Mocha/src/Mocha/Builder/SagaRegistration.cs
index 71e420fb5d2..80ea7090a06 100644
--- a/src/Mocha/src/Mocha/Builder/SagaRegistration.cs
+++ b/src/Mocha/src/Mocha/Builder/SagaRegistration.cs
@@ -16,4 +16,10 @@ internal sealed class SagaRegistration
/// Factory that creates the saga instance.
///
public required Func Factory { get; init; }
+
+ ///
+ /// Optional pre-built state serializer provided by the source generator.
+ /// When set, the saga uses this serializer instead of resolving one from the factory.
+ ///
+ public ISagaStateSerializer? StateSerializer { get; init; }
}
diff --git a/src/Mocha/src/Mocha/Configuration/MessagingMessageConfiguration.cs b/src/Mocha/src/Mocha/Configuration/MessagingMessageConfiguration.cs
new file mode 100644
index 00000000000..dc4a44a4b57
--- /dev/null
+++ b/src/Mocha/src/Mocha/Configuration/MessagingMessageConfiguration.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel;
+
+namespace Mocha;
+
+///
+/// Pre-built message type configuration emitted by the source generator.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+public sealed class MessagingMessageConfiguration
+{
+ ///
+ /// The CLR type of the message.
+ ///
+ public required Type MessageType { get; init; }
+
+ ///
+ /// The pre-built serializer for this message type.
+ ///
+ public required IMessageSerializer Serializer { get; init; }
+
+ ///
+ /// Pre-computed type hierarchy sorted by specificity (most specific first).
+ /// Contains only registered user types — framework base types (,
+ /// ) are excluded. When non-null,
+ /// skips reflection-based hierarchy discovery.
+ ///
+ public Type[]? EnclosedTypes { get; init; }
+}
diff --git a/src/Mocha/src/Mocha/Configuration/MessagingSagaConfiguration.cs b/src/Mocha/src/Mocha/Configuration/MessagingSagaConfiguration.cs
new file mode 100644
index 00000000000..49fc80080c4
--- /dev/null
+++ b/src/Mocha/src/Mocha/Configuration/MessagingSagaConfiguration.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel;
+using Mocha.Sagas;
+
+namespace Mocha;
+
+///
+/// Pre-built saga configuration emitted by the source generator.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+public sealed class MessagingSagaConfiguration
+{
+ ///
+ /// The CLR type of the saga.
+ ///
+ public required Type SagaType { get; init; }
+
+ ///
+ /// The pre-built state serializer for this saga.
+ ///
+ public required ISagaStateSerializer StateSerializer { get; init; }
+}
diff --git a/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs b/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs
index 425ab18c6bf..505ddf2014b 100644
--- a/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs
+++ b/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs
@@ -149,11 +149,7 @@ protected virtual VisitorAction VisitTransport(MessagingTransport transport, TCo
protected virtual VisitorAction VisitSaga(SagaConsumer consumer, TContext context)
{
- var saga = GetSagaFromConsumer(consumer);
- if (saga is null)
- {
- return VisitorAction.Continue;
- }
+ var saga = consumer.Saga;
var action = Enter(saga, context);
if (action == VisitorAction.Break)
@@ -170,22 +166,6 @@ protected virtual VisitorAction VisitSaga(SagaConsumer consumer, TContext contex
return VisitorAction.Continue;
}
- private static Saga? GetSagaFromConsumer(SagaConsumer consumer)
- {
- var fields = typeof(SagaConsumer).GetFields(
- System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
-
- foreach (var field in fields)
- {
- if (typeof(Saga).IsAssignableFrom(field.FieldType))
- {
- return field.GetValue(consumer) as Saga;
- }
- }
-
- return null;
- }
-
protected virtual VisitorAction Enter(MessagingRuntime runtime, TContext context) => VisitorAction.Continue;
protected virtual VisitorAction Leave(MessagingRuntime runtime, TContext context) => VisitorAction.Continue;
diff --git a/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs b/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs
index 62045a64ad4..712e41469d1 100644
--- a/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs
+++ b/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Mocha.Middlewares;
@@ -18,6 +19,8 @@ public static class MessageBusHostBuilderExtensions
/// The event handler type.
/// The host builder.
/// The builder for method chaining.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
public static IMessageBusHostBuilder AddEventHandler<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
this IMessageBusHostBuilder builder)
@@ -48,6 +51,53 @@ public static IMessageBusHostBuilder AddHandlerConfiguration<
return builder;
}
+ ///
+ /// Registers a message type using pre-built configuration from the source generator.
+ ///
+ /// The host builder.
+ /// The pre-built message configuration.
+ /// The builder for method chaining.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static IMessageBusHostBuilder AddMessageConfiguration(
+ this IMessageBusHostBuilder builder,
+ MessagingMessageConfiguration configuration)
+ {
+ builder.ConfigureMessageBus(h => h.AddMessageConfiguration(configuration));
+ return builder;
+ }
+
+ ///
+ /// Registers a saga using pre-built configuration from the source generator.
+ ///
+ /// The saga type.
+ /// The host builder.
+ /// The pre-built saga configuration.
+ /// The builder for method chaining.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static IMessageBusHostBuilder AddSagaConfiguration(
+ this IMessageBusHostBuilder builder,
+ MessagingSagaConfiguration configuration)
+ where TSaga : Saga, new()
+ {
+ builder.ConfigureMessageBus(h => h.AddSagaConfiguration(configuration));
+ return builder;
+ }
+
+ ///
+ /// Registers a JSON type info resolver for AOT-compatible serialization.
+ ///
+ /// The host builder.
+ /// The JSON type info resolver to register.
+ /// The builder for method chaining.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static IMessageBusHostBuilder AddJsonTypeInfoResolver(
+ this IMessageBusHostBuilder builder,
+ IJsonTypeInfoResolver resolver)
+ {
+ builder.ConfigureMessageBus(h => h.ConfigureServices(services => services.AddSingleton(resolver)));
+ return builder;
+ }
+
///
/// Registers a batch event handler with the message bus and adds it to the service collection.
///
@@ -55,6 +105,8 @@ public static IMessageBusHostBuilder AddHandlerConfiguration<
/// The host builder.
/// Optional action to configure batch options.
/// The builder for method chaining.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
public static IMessageBusHostBuilder AddBatchHandler<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
this IMessageBusHostBuilder builder,
@@ -73,7 +125,10 @@ public static IMessageBusHostBuilder AddBatchHandler<
/// The saga type.
/// The host builder.
/// The builder for method chaining.
- public static IMessageBusHostBuilder AddSaga(this IMessageBusHostBuilder builder) where TSaga : Saga, new()
+ public static IMessageBusHostBuilder AddSaga<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TSaga>(
+ this IMessageBusHostBuilder builder)
+ where TSaga : Saga, new()
{
builder.ConfigureMessageBus(static h => h.AddSaga());
return builder;
@@ -85,7 +140,11 @@ public static IMessageBusHostBuilder AddBatchHandler<
/// The request handler type.
/// The host builder.
/// The builder for method chaining.
- public static IMessageBusHostBuilder AddRequestHandler(this IMessageBusHostBuilder builder)
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ public static IMessageBusHostBuilder AddRequestHandler<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
+ this IMessageBusHostBuilder builder)
where THandler : class, IEventRequestHandler
{
builder.Services.TryAddScoped();
@@ -100,6 +159,8 @@ public static IMessageBusHostBuilder AddRequestHandler(this IMessageBu
/// The consumer type implementing .
/// The host builder.
/// The builder for method chaining.
+ [RequiresDynamicCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
+ [RequiresUnreferencedCode("Use source-generated AddHandlerConfiguration for AOT compatibility.")]
public static IMessageBusHostBuilder AddConsumer<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConsumer>(
this IMessageBusHostBuilder builder)
diff --git a/src/Mocha/src/Mocha/Extensions/TypesExtensions.cs b/src/Mocha/src/Mocha/Extensions/TypesExtensions.cs
index 473828d6916..cd7dbfbe90c 100644
--- a/src/Mocha/src/Mocha/Extensions/TypesExtensions.cs
+++ b/src/Mocha/src/Mocha/Extensions/TypesExtensions.cs
@@ -3,5 +3,5 @@ namespace Mocha;
internal static class TypesExtensions
{
public static bool IsEventRequest(this Type type)
- => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEventRequest<>));
+ => typeof(IEventRequest).IsAssignableFrom(type);
}
diff --git a/src/Mocha/src/Mocha/IMessageBus.cs b/src/Mocha/src/Mocha/IMessageBus.cs
index c74207a6611..2f3fab33a40 100644
--- a/src/Mocha/src/Mocha/IMessageBus.cs
+++ b/src/Mocha/src/Mocha/IMessageBus.cs
@@ -12,7 +12,7 @@ public interface IMessageBus
/// The message instance to publish.
/// A token to cancel the publish operation.
/// A task that completes when the message has been handed off to the transport.
- ValueTask PublishAsync(T message, CancellationToken cancellationToken);
+ ValueTask PublishAsync(T message, CancellationToken cancellationToken) where T : notnull;
///
/// Publishes a message to all subscribers of the specified message type with additional publish options.
@@ -22,24 +22,26 @@ public interface IMessageBus
/// Options controlling publish behavior such as headers and expiration.
/// A token to cancel the publish operation.
/// A task that completes when the message has been handed off to the transport.
- ValueTask PublishAsync(T message, PublishOptions options, CancellationToken cancellationToken);
+ ValueTask PublishAsync(T message, PublishOptions options, CancellationToken cancellationToken) where T : notnull;
///
/// Sends a message to a single receiver determined by the message type's routing configuration.
///
+ /// The type of the message to send.
/// The message instance to send.
/// A token to cancel the send operation.
/// A task that completes when the message has been handed off to the transport.
- ValueTask SendAsync(object message, CancellationToken cancellationToken);
+ ValueTask SendAsync(T message, CancellationToken cancellationToken) where T : notnull;
///
/// Sends a message to a single receiver with additional send options.
///
+ /// The type of the message to send.
/// The message instance to send.
/// Options controlling send behavior such as headers and expiration.
/// A token to cancel the send operation.
/// A task that completes when the message has been handed off to the transport.
- ValueTask SendAsync(object message, SendOptions options, CancellationToken cancellationToken);
+ ValueTask SendAsync(T message, SendOptions options, CancellationToken cancellationToken) where T : notnull;
///
/// Sends a request message and waits for a typed response from the handler.
@@ -106,7 +108,8 @@ ValueTask ReplyAsync(TResponse response, ReplyOptions options, Cancel
ValueTask SchedulePublishAsync(
T message,
DateTimeOffset scheduledTime,
- CancellationToken cancellationToken) where T : notnull;
+ CancellationToken cancellationToken)
+ where T : notnull;
///
/// Publishes a message scheduled for delivery at the specified time with additional options.
@@ -121,33 +124,38 @@ ValueTask SchedulePublishAsync(
T message,
DateTimeOffset scheduledTime,
PublishOptions options,
- CancellationToken cancellationToken) where T : notnull;
+ CancellationToken cancellationToken)
+ where T : notnull;
///
/// Sends a message scheduled for delivery at the specified time.
///
+ /// The type of the message to send.
/// The message instance to send.
/// The absolute time at which the message should be delivered.
/// A token to cancel the send operation.
/// A scheduling result containing the cancellation token and metadata.
- ValueTask ScheduleSendAsync(
- object message,
+ ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
- CancellationToken cancellationToken);
+ CancellationToken cancellationToken)
+ where T : notnull;
///
/// Sends a message scheduled for delivery at the specified time with additional options.
///
+ /// The type of the message to send.
/// The message instance to send.
/// The absolute time at which the message should be delivered.
/// Options controlling send behavior such as headers and expiration.
/// A token to cancel the send operation.
/// A scheduling result containing the cancellation token and metadata.
- ValueTask ScheduleSendAsync(
- object message,
+ ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
SendOptions options,
- CancellationToken cancellationToken);
+ CancellationToken cancellationToken)
+ where T : notnull;
///
/// Cancels a previously scheduled message. Returns true if the message was cancelled,
@@ -156,7 +164,5 @@ ValueTask ScheduleSendAsync(
/// The opaque scheduling token returned by a prior schedule operation.
/// A token to cancel the cancellation operation.
/// true if the scheduled message was cancelled; otherwise false.
- ValueTask CancelScheduledMessageAsync(
- string token,
- CancellationToken cancellationToken);
+ ValueTask CancelScheduledMessageAsync(string token, CancellationToken cancellationToken);
}
diff --git a/src/Mocha/src/Mocha/MessageTypes/Configurations/MessageTypeConfiguration.cs b/src/Mocha/src/Mocha/MessageTypes/Configurations/MessageTypeConfiguration.cs
index 197e5ce4237..77db63d870c 100644
--- a/src/Mocha/src/Mocha/MessageTypes/Configurations/MessageTypeConfiguration.cs
+++ b/src/Mocha/src/Mocha/MessageTypes/Configurations/MessageTypeConfiguration.cs
@@ -34,4 +34,9 @@ public class MessageTypeConfiguration : MessagingConfiguration
/// Gets or sets a value indicating whether this message type is internal (not exposed for external routing).
///
public bool IsInternal { get; set; }
+
+ ///
+ /// Pre-computed enclosed types from source generator.
+ ///
+ public Type[]? EnclosedTypes { get; set; }
}
diff --git a/src/Mocha/src/Mocha/MessageTypes/Descriptors/MessageTypeDescriptor.cs b/src/Mocha/src/Mocha/MessageTypes/Descriptors/MessageTypeDescriptor.cs
index 0cd04f9b6e1..aa93868567a 100644
--- a/src/Mocha/src/Mocha/MessageTypes/Descriptors/MessageTypeDescriptor.cs
+++ b/src/Mocha/src/Mocha/MessageTypes/Descriptors/MessageTypeDescriptor.cs
@@ -25,15 +25,14 @@ public MessageTypeDescriptor(IMessagingConfigurationContext context, Type messag
/// The configured .
public MessageTypeConfiguration CreateConfiguration()
{
- var routes = _routes.Select(r => r.CreateConfiguration()).ToList();
- Configuration.Routes = routes;
+ Configuration.Routes = _routes.ConvertAll(r => r.CreateConfiguration());
return Configuration;
}
///
public IMessageTypeDescriptor AddSerializer(IMessageSerializer messageSerializer)
{
- Configuration.MessageSerializer.Add(messageSerializer.ContentType, messageSerializer);
+ Configuration.MessageSerializer[messageSerializer.ContentType] = messageSerializer;
return this;
}
diff --git a/src/Mocha/src/Mocha/MessageTypes/MessageType.cs b/src/Mocha/src/Mocha/MessageTypes/MessageType.cs
index ad470f2f5b6..94cbcda0eab 100644
--- a/src/Mocha/src/Mocha/MessageTypes/MessageType.cs
+++ b/src/Mocha/src/Mocha/MessageTypes/MessageType.cs
@@ -1,4 +1,6 @@
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
using Mocha.Features;
namespace Mocha;
@@ -13,10 +15,14 @@ public sealed class MessageType
///
public bool IsCompleted { get; private set; }
+ private Type[]? _enclosedTypes;
+
private IMessageSerializerRegistry _serializerRegistry = null!;
- private ImmutableDictionary _serializer
- = ImmutableDictionary.Empty;
+ private ImmutableDictionary _serializer = ImmutableDictionary<
+ MessageContentType,
+ IMessageSerializer
+ >.Empty;
///
/// Gets the URN-based identity string that uniquely identifies this message type on the wire.
@@ -84,6 +90,8 @@ public void Initialize(IMessagingConfigurationContext context, MessageTypeConfig
_serializer = configuration.MessageSerializer.ToImmutableDictionary(k => k.Key, v => v.Value);
+ _enclosedTypes = configuration.EnclosedTypes;
+
foreach (var routeConfiguration in configuration.Routes)
{
var outboundRoute = new OutboundRoute();
@@ -118,47 +126,81 @@ public void Initialize(IMessagingConfigurationContext context, MessageTypeConfig
/// Completes initialization by resolving the full type hierarchy and registering enclosed message types.
///
/// The messaging configuration context.
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2075",
+ Justification = "Reflection path only executes when _enclosedTypes is null (non-AOT). AOT users provide enclosed types via source generator.")]
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
+ Justification = "Reflection path only executes when _enclosedTypes is null (non-AOT). AOT users provide enclosed types via source generator.")]
public void Complete(IMessagingConfigurationContext context)
{
- var allTypes = GetAllTypesInHierarchy(RuntimeType, context);
-
- // Sort by specificity (most specific first)
- var sortedTypes = allTypes
- .OrderByDescending(t => allTypes.Count(other => t != other && t.IsAssignableTo(other)))
- .ToList();
-
var enclosedMessageTypes = ImmutableArray.CreateBuilder();
var enclosedMessageIdentities = ImmutableArray.CreateBuilder();
- foreach (var type in sortedTypes)
+ if (_enclosedTypes is not null)
{
- if (IsFrameworkBaseType(type))
- {
- // Don't register framework base types as standalone message types.
- // Only include their identity string for wire-level message matching.
- enclosedMessageIdentities.Add(context.Naming.GetMessageIdentity(type));
- }
- else
+ // AOT path: pre-sorted registered types only, no framework types
+ foreach (var type in _enclosedTypes)
{
var mt = context.Messages.GetOrAdd(context, type);
enclosedMessageTypes.Add(mt);
enclosedMessageIdentities.Add(mt.Identity);
}
+
+ // Response types already registered by generator — skip discovery
}
+ else
+ {
+ var options = context.Services.GetRequiredService();
- EnclosedMessageTypes = enclosedMessageTypes.ToImmutableArray();
- EnclosedMessageIdentities = enclosedMessageIdentities.ToImmutableArray();
+ if (options.IsAotCompatible)
+ {
+ throw new InvalidOperationException(
+ $"No enclosed types provided for message type '{Identity}'. "
+ + "Register enclosed types via the source generator. "
+ + "Set IsAotCompatible = false to allow reflection-based type discovery.");
+ }
- var interfaces = RuntimeType.GetInterfaces();
- foreach (var interfaceType in interfaces)
- {
- if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEventRequest<>))
+ // Reflection path: existing code, completely unchanged
+ var allTypes = GetAllTypesInHierarchy(RuntimeType, context);
+
+ // Sort by specificity (most specific first)
+ var sortedTypes = allTypes
+ .OrderByDescending(t => allTypes.Count(other => t != other && t.IsAssignableTo(other)))
+ .ToList();
+
+ foreach (var type in sortedTypes)
+ {
+ if (IsFrameworkBaseType(type))
+ {
+ // Don't register framework base types as standalone message types.
+ // Only include their identity string for wire-level message matching.
+ enclosedMessageIdentities.Add(context.Naming.GetMessageIdentity(type));
+ }
+ else
+ {
+ var mt = context.Messages.GetOrAdd(context, type);
+ enclosedMessageTypes.Add(mt);
+ enclosedMessageIdentities.Add(mt.Identity);
+ }
+ }
+
+ foreach (var interfaceType in RuntimeType.GetInterfaces())
{
- var responseType = interfaceType.GetGenericArguments()[0];
- context.Messages.GetOrAdd(context, responseType);
+ if (interfaceType.IsGenericType
+ && interfaceType.GetGenericTypeDefinition() == typeof(IEventRequest<>))
+ {
+ var responseType = interfaceType.GetGenericArguments()[0];
+ context.Messages.GetOrAdd(context, responseType);
+ }
}
}
+ EnclosedMessageTypes = enclosedMessageTypes.ToImmutableArray();
+ EnclosedMessageIdentities = enclosedMessageIdentities.ToImmutableArray();
+
IsCompleted = true;
}
@@ -178,6 +220,7 @@ public MessageTypeDescription Describe()
EnclosedMessageIdentities.IsDefaultOrEmpty ? null : EnclosedMessageIdentities);
}
+ [RequiresUnreferencedCode("Uses GetInterfaces and BaseType traversal which may reference trimmed types.")]
private static List GetAllTypesInHierarchy(Type type, IMessagingConfigurationContext context)
{
var interfaces = type.GetInterfaces();
diff --git a/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs b/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs
index c3996080adb..de980a5c5b7 100644
--- a/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs
+++ b/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs
@@ -36,6 +36,7 @@ public sealed class DefaultMessageBus(
/// The message instance to publish. Must not be .
/// A token to cancel the publish operation.
public async ValueTask PublishAsync(T message, CancellationToken cancellationToken)
+ where T : notnull
{
await PublishAsync(message, PublishOptions.Default, cancellationToken);
}
@@ -53,8 +54,9 @@ public async ValueTask PublishAsync(T message, CancellationToken cancellation
/// Options controlling headers and expiration for this publish operation.
/// A token to cancel the publish operation.
public async ValueTask PublishAsync(T message, PublishOptions options, CancellationToken cancellationToken)
+ where T : notnull
{
- var messageType = runtime.GetMessageType(message!.GetType());
+ var messageType = runtime.GetMessageType(message.GetType());
var endpoint = runtime.GetPublishEndpoint(messageType);
var context = _contextPool.Get();
@@ -79,9 +81,11 @@ public async ValueTask PublishAsync(T message, PublishOptions options, Cancel
///
/// Sends a message to a single consumer endpoint using default send options.
///
+ /// The type of the message to send.
/// The message instance to send. Must not be .
/// A token to cancel the send operation.
- public ValueTask SendAsync(object message, CancellationToken cancellationToken)
+ public ValueTask SendAsync(T message, CancellationToken cancellationToken)
+ where T : notnull
{
return SendAsync(message, SendOptions.Default, cancellationToken);
}
@@ -94,10 +98,12 @@ public ValueTask SendAsync(object message, CancellationToken cancellationToken)
/// address; otherwise the runtime's router resolves the endpoint by message type. Reply and fault
/// addresses from the options are propagated to the dispatch context.
///
+ /// The type of the message to send.
/// The message instance to send. Must not be .
/// Options controlling the target endpoint, headers, reply/fault addresses, and expiration.
/// A token to cancel the send operation.
- public async ValueTask SendAsync(object message, SendOptions options, CancellationToken cancellationToken)
+ public async ValueTask SendAsync(T message, SendOptions options, CancellationToken cancellationToken)
+ where T : notnull
{
var messageType = runtime.GetMessageType(message.GetType());
var endpoint = options.Endpoint is { } address
@@ -316,7 +322,7 @@ public async ValueTask SchedulePublishAsync(
CancellationToken cancellationToken)
where T : notnull
{
- var messageType = runtime.GetMessageType(message!.GetType());
+ var messageType = runtime.GetMessageType(message.GetType());
var endpoint = runtime.GetPublishEndpoint(messageType);
var context = _contextPool.Get();
@@ -350,10 +356,11 @@ public async ValueTask SchedulePublishAsync(
///
/// Sends a message scheduled for delivery at the specified time using default options.
///
- public async ValueTask ScheduleSendAsync(
- object message,
+ public async ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
CancellationToken cancellationToken)
+ where T : notnull
{
return await ScheduleSendAsync(message, scheduledTime, SendOptions.Default, cancellationToken);
}
@@ -361,11 +368,12 @@ public async ValueTask ScheduleSendAsync(
///
/// Sends a message scheduled for delivery at the specified time with additional options.
///
- public async ValueTask ScheduleSendAsync(
- object message,
+ public async ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
SendOptions options,
CancellationToken cancellationToken)
+ where T : notnull
{
var messageType = runtime.GetMessageType(message.GetType());
var endpoint = options.Endpoint is { } address
diff --git a/src/Mocha/src/Mocha/Mocha.csproj b/src/Mocha/src/Mocha/Mocha.csproj
index 32283bbe436..b88556d3910 100644
--- a/src/Mocha/src/Mocha/Mocha.csproj
+++ b/src/Mocha/src/Mocha/Mocha.csproj
@@ -3,6 +3,7 @@
Mocha
Mocha
enable
+ true
diff --git a/src/Mocha/src/Mocha/Observability/Configuration/InstrumentationBusExtensions.cs b/src/Mocha/src/Mocha/Observability/Configuration/InstrumentationBusExtensions.cs
index d9e64ef62ea..91d6d9306bc 100644
--- a/src/Mocha/src/Mocha/Observability/Configuration/InstrumentationBusExtensions.cs
+++ b/src/Mocha/src/Mocha/Observability/Configuration/InstrumentationBusExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -24,7 +25,9 @@ public static IMessageBusHostBuilder AddInstrumentation(this IMessageBusHostBuil
///
/// Registers a custom implementation.
///
- public static IMessageBusHostBuilder AddDiagnosticEventListener(this IMessageBusHostBuilder builder)
+ public static IMessageBusHostBuilder AddDiagnosticEventListener<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
+ this IMessageBusHostBuilder builder)
where T : class, IMessagingDiagnosticEventListener
{
ArgumentNullException.ThrowIfNull(builder);
diff --git a/src/Mocha/src/Mocha/Sagas/Saga.Initialization.cs b/src/Mocha/src/Mocha/Sagas/Saga.Initialization.cs
index 9fc4a607d18..866ba5428c0 100644
--- a/src/Mocha/src/Mocha/Sagas/Saga.Initialization.cs
+++ b/src/Mocha/src/Mocha/Sagas/Saga.Initialization.cs
@@ -29,7 +29,7 @@ public override void Initialize(IMessagingSetupContext context)
_logger = context.Services.GetRequiredService>>();
Name = definition.Name ?? throw new SagaInitializationException(this, "Saga name is not defined.");
- StateSerializer =
+ StateSerializer ??=
definition.StateSerializer?.Invoke(context.Services)
?? context.Services.GetRequiredService().GetSerializer(typeof(TState));
diff --git a/src/Mocha/src/Mocha/Sagas/Saga.cs b/src/Mocha/src/Mocha/Sagas/Saga.cs
index 03eb8b016b3..49c82377793 100644
--- a/src/Mocha/src/Mocha/Sagas/Saga.cs
+++ b/src/Mocha/src/Mocha/Sagas/Saga.cs
@@ -37,7 +37,7 @@ protected Saga()
///
/// Gets the serializer used to persist and restore saga state.
///
- public ISagaStateSerializer StateSerializer { get; protected set; } = null!;
+ public ISagaStateSerializer StateSerializer { get; protected internal set; } = null!;
///
/// Gets the logical name of this saga, used for logging, diagnostics, and state store identification.
diff --git a/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs b/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs
index 9aaacdf6d95..d61da1cfec5 100644
--- a/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs
+++ b/src/Mocha/src/Mocha/Sagas/SagaEventListener.cs
@@ -9,6 +9,11 @@ namespace Mocha.Sagas;
/// The saga definition that this consumer handles.
public sealed class SagaConsumer(Saga saga) : Consumer
{
+ ///
+ /// Gets the saga definition that this consumer handles.
+ ///
+ public Saga Saga => saga;
+
///
protected override void Configure(IConsumerDescriptor descriptor)
{
diff --git a/src/Mocha/src/Mocha/Sagas/State/Serialization/JsonSagaStateSerializerFactory.cs b/src/Mocha/src/Mocha/Sagas/State/Serialization/JsonSagaStateSerializerFactory.cs
index 7fe82d182b2..f54420cade9 100644
--- a/src/Mocha/src/Mocha/Sagas/State/Serialization/JsonSagaStateSerializerFactory.cs
+++ b/src/Mocha/src/Mocha/Sagas/State/Serialization/JsonSagaStateSerializerFactory.cs
@@ -1,14 +1,24 @@
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace Mocha.Sagas;
-internal sealed class JsonSagaStateSerializerFactory(IEnumerable typeInfos)
- : ISagaStateSerializerFactory
+internal sealed class JsonSagaStateSerializerFactory(
+ IEnumerable typeInfos,
+ IReadOnlyMessagingOptions options) : ISagaStateSerializerFactory
{
private readonly ImmutableArray _typeInfos = [.. typeInfos];
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026:RequiresUnreferencedCode",
+ Justification = "Reflection fallback only used when no JsonSerializerContext is provided.")]
+ [UnconditionalSuppressMessage(
+ "AOT",
+ "IL3050:RequiresDynamicCode",
+ Justification = "Reflection fallback only used when no JsonSerializerContext is provided.")]
public ISagaStateSerializer GetSerializer(Type type)
{
JsonTypeInfo? typeInfo = null;
@@ -22,7 +32,18 @@ public ISagaStateSerializer GetSerializer(Type type)
}
}
- typeInfo ??= JsonSerializerOptions.Default.GetTypeInfo(type);
+ if (typeInfo is null)
+ {
+ if (options.IsAotCompatible)
+ {
+ throw new InvalidOperationException(
+ $"No JsonTypeInfo found for saga state type '{type.Name}'. "
+ + "Register it via [JsonSerializable] on your JsonSerializerContext. "
+ + "Set IsAotCompatible = false to allow reflection-based serialization.");
+ }
+
+ typeInfo = JsonSerializerOptions.Default.GetTypeInfo(type);
+ }
return new JsonSagaStateSerializer(typeInfo);
}
diff --git a/src/Mocha/src/Mocha/Serialization/HeadersConverter.cs b/src/Mocha/src/Mocha/Serialization/HeadersConverter.cs
index 4f3f6308e63..96101dea13b 100644
--- a/src/Mocha/src/Mocha/Serialization/HeadersConverter.cs
+++ b/src/Mocha/src/Mocha/Serialization/HeadersConverter.cs
@@ -237,9 +237,9 @@ private static void WriteValue(Utf8JsonWriter writer, object? value, JsonSeriali
break;
default:
- // Fallback to default serialization for unknown types
- JsonSerializer.Serialize(writer, value, value.GetType(), options);
- break;
+ throw new InvalidOperationException(
+ $"Header value type '{value.GetType().Name}' is not supported for serialization. "
+ + "Headers must contain primitive types (string, int, long, double, bool, DateTime, etc.).");
}
}
}
diff --git a/src/Mocha/src/Mocha/Serialization/JsonMessageSerializer.cs b/src/Mocha/src/Mocha/Serialization/JsonMessageSerializer.cs
index 1e4516f60db..82f292d07e2 100644
--- a/src/Mocha/src/Mocha/Serialization/JsonMessageSerializer.cs
+++ b/src/Mocha/src/Mocha/Serialization/JsonMessageSerializer.cs
@@ -4,7 +4,7 @@
namespace Mocha;
-internal sealed class JsonMessageSerializer(JsonTypeInfo typeInfo) : IMessageSerializer
+public sealed class JsonMessageSerializer(JsonTypeInfo typeInfo) : IMessageSerializer
{
public MessageContentType ContentType => MessageContentType.Json;
diff --git a/src/Mocha/src/Mocha/Serialization/JsonMessageSerializerFactory.cs b/src/Mocha/src/Mocha/Serialization/JsonMessageSerializerFactory.cs
index a0f9484e3aa..585a6c3a3ba 100644
--- a/src/Mocha/src/Mocha/Serialization/JsonMessageSerializerFactory.cs
+++ b/src/Mocha/src/Mocha/Serialization/JsonMessageSerializerFactory.cs
@@ -1,16 +1,26 @@
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace Mocha;
-internal sealed class JsonMessageSerializerFactory(IEnumerable typeInfos)
- : IMessageSerializerFactory
+internal sealed class JsonMessageSerializerFactory(
+ IEnumerable typeInfos,
+ IReadOnlyMessagingOptions options) : IMessageSerializerFactory
{
private readonly ImmutableArray _typeInfos = [.. typeInfos];
public MessageContentType ContentType { get; } = MessageContentType.Json;
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026",
+ Justification = "Fallback path; AOT users provide JsonSerializerContext via MessagingModule attribute.")]
+ [UnconditionalSuppressMessage(
+ "AOT",
+ "IL3050",
+ Justification = "Fallback path; AOT users provide JsonSerializerContext via MessagingModule attribute.")]
public IMessageSerializer GetSerializer(Type type)
{
JsonTypeInfo? typeInfo = null;
@@ -24,7 +34,18 @@ public IMessageSerializer GetSerializer(Type type)
}
}
- typeInfo ??= JsonSerializerOptions.Default.GetTypeInfo(type);
+ if (typeInfo is null)
+ {
+ if (options.IsAotCompatible)
+ {
+ throw new InvalidOperationException(
+ $"No JsonTypeInfo found for type '{type.FullName}'. "
+ + "Register it via [JsonSerializable] on your JsonSerializerContext. "
+ + "Set IsAotCompatible = false to allow reflection-based serialization.");
+ }
+
+ typeInfo = JsonSerializerOptions.Default.GetTypeInfo(type);
+ }
return new JsonMessageSerializer(typeInfo);
}
diff --git a/src/Mocha/src/Mocha/Serialization/MessageTypeRegistry.cs b/src/Mocha/src/Mocha/Serialization/MessageTypeRegistry.cs
index 922b653769e..d36694c8111 100644
--- a/src/Mocha/src/Mocha/Serialization/MessageTypeRegistry.cs
+++ b/src/Mocha/src/Mocha/Serialization/MessageTypeRegistry.cs
@@ -6,7 +6,10 @@ namespace Mocha;
/// Thread-safe implementation of that stores and resolves message type metadata by CLR type and identity string.
///
/// The serializer registry used to resolve serializers for message types.
-public sealed class MessageTypeRegistry(IMessageSerializerRegistry serializerRegistry) : IMessageTypeRegistry
+/// The messaging options controlling strict registration mode.
+public sealed class MessageTypeRegistry(
+ IMessageSerializerRegistry serializerRegistry,
+ IReadOnlyMessagingOptions options) : IMessageTypeRegistry
{
public IMessageSerializerRegistry Serializers => serializerRegistry;
@@ -56,6 +59,14 @@ public MessageType GetOrAdd(IMessagingConfigurationContext context, Type type)
return messageType;
}
+ if (options.IsAotCompatible)
+ {
+ throw new InvalidOperationException(
+ $"Message type '{type.FullName}' was not registered at startup. "
+ + "Register it via the source generator or AddMessageConfiguration(). "
+ + "Set IsAotCompatible = false to allow runtime type registration.");
+ }
+
lock (_lock)
{
messageType = GetMessageType(type);
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs
index 5f64289b575..ff13ba61a5b 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/DiagnosticTests.cs
@@ -169,6 +169,27 @@ public ValueTask HandleAsync(GenericQuery query, CancellationToken cancell
]).MatchMarkdownAsync();
}
+ [Fact]
+ public async Task MO0006_OpenGenericHandlerWithConcreteMessage_ReportsInfo()
+ {
+ await TestHelper.GetGeneratedSourceSnapshot(
+ [
+ """
+ using Mocha.Mediator;
+
+ namespace TestApp;
+
+ public record MyCommand : ICommand;
+
+ public class GenericHandler : ICommandHandler
+ {
+ public ValueTask HandleAsync(MyCommand command, CancellationToken cancellationToken)
+ => default;
+ }
+ """
+ ]).MatchMarkdownAsync();
+ }
+
[Fact]
public async Task MO0005_CommandAndNotificationHandler_ReportsError()
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md
index 654d92630ef..15e3d83f87f 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_CommandWithResponseHandler_MatchesSnapshot.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderCommand),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md
index 00348330827..6c0632f158f 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_MultipleCommandHandlers_MatchesSnapshot.md
@@ -11,6 +11,19 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderCommand),
+ typeof(global::TestApp.DeleteOrderCommand),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderHandler),
+ typeof(global::TestApp.DeleteOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md
index 6df93870771..14b6a82ce6b 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/CommandHandlerGeneratorTests.Generate_VoidCommandHandler_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md
index ba5dc5bd496..d8df9d3acb3 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_CommandWithTwoHandlers_ReportsError.md
@@ -13,6 +13,18 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderCommand),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderHandlerA),
+ typeof(global::TestApp.CreateOrderHandlerB),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md
index d4b08ea56db..bbdfcaec3ab 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0002_VoidCommandWithTwoHandlers_ReportsError.md
@@ -13,6 +13,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderHandlerA),
+ typeof(global::TestApp.DeleteOrderHandlerB),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md
index 7648ba7cb15..5aa4ecae8aa 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericCommand_ReportsInfo.md
@@ -1,41 +1,5 @@
# MO0004_OpenGenericCommand_ReportsInfo
-## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs
-
-```csharp
-//
-
-#nullable enable
-#pragma warning disable
-
-namespace Microsoft.Extensions.DependencyInjection
-{
- [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
- public static class TestsMediatorBuilderExtensions
- {
- public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
- this global::Mocha.Mediator.IMediatorHostBuilder builder)
- {
-
- // Register handler configurations
- global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration>(builder,
- new global::Mocha.Mediator.MediatorHandlerConfiguration
- {
- HandlerType = typeof(global::TestApp.GenericCommandHandler),
- MessageType = typeof(global::TestApp.GenericCommand),
- Kind = global::Mocha.Mediator.MediatorHandlerKind.Command,
- Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline, global::TestApp.GenericCommand>()
- });
-
- return builder;
- }
- }
-}
-
-```
-
-## Generator Diagnostics
-
```json
[
{
@@ -48,6 +12,17 @@ namespace Microsoft.Extensions.DependencyInjection
"Message": "Message type 'global::TestApp.GenericCommand' is an open generic and cannot be dispatched at runtime",
"Category": "Mediator",
"CustomTags": []
+ },
+ {
+ "Id": "MO0006",
+ "Title": "Open generic handler cannot be auto-registered",
+ "Severity": "Info",
+ "WarningLevel": 1,
+ "Location": ": (6,13)-(6,34)",
+ "MessageFormat": "Handler '{0}' is an open generic and cannot be auto-registered",
+ "Message": "Handler 'global::TestApp.GenericCommandHandler' is an open generic and cannot be auto-registered",
+ "Category": "Mediator",
+ "CustomTags": []
}
]
```
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md
index 79a9e35d339..f2ae383928f 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0004_OpenGenericQuery_ReportsInfo.md
@@ -1,42 +1,5 @@
# MO0004_OpenGenericQuery_ReportsInfo
-## TestsMediatorBuilderExtensions.kHkt5Slhw0EY-Xbr5K86dQ.g.cs
-
-```csharp
-//
-
-#nullable enable
-#pragma warning disable
-
-namespace Microsoft.Extensions.DependencyInjection
-{
- [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
- public static class TestsMediatorBuilderExtensions
- {
- public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
- this global::Mocha.Mediator.IMediatorHostBuilder builder)
- {
-
- // Register handler configurations
- global::Mocha.Mediator.MediatorHostBuilderHandlerExtensions.AddHandlerConfiguration>(builder,
- new global::Mocha.Mediator.MediatorHandlerConfiguration
- {
- HandlerType = typeof(global::TestApp.GenericQueryHandler),
- MessageType = typeof(global::TestApp.GenericQuery),
- ResponseType = typeof(T),
- Kind = global::Mocha.Mediator.MediatorHandlerKind.Query,
- Delegate = global::Mocha.Mediator.PipelineBuilder.BuildQueryPipeline, global::TestApp.GenericQuery, T>()
- });
-
- return builder;
- }
- }
-}
-
-```
-
-## Generator Diagnostics
-
```json
[
{
@@ -49,6 +12,17 @@ namespace Microsoft.Extensions.DependencyInjection
"Message": "Message type 'global::TestApp.GenericQuery' is an open generic and cannot be dispatched at runtime",
"Category": "Mediator",
"CustomTags": []
+ },
+ {
+ "Id": "MO0006",
+ "Title": "Open generic handler cannot be auto-registered",
+ "Severity": "Info",
+ "WarningLevel": 1,
+ "Location": ": (6,13)-(6,32)",
+ "MessageFormat": "Handler '{0}' is an open generic and cannot be auto-registered",
+ "Message": "Handler 'global::TestApp.GenericQueryHandler' is an open generic and cannot be auto-registered",
+ "Category": "Mediator",
+ "CustomTags": []
}
]
```
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0006_OpenGenericHandlerWithConcreteMessage_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0006_OpenGenericHandlerWithConcreteMessage_ReportsInfo.md
new file mode 100644
index 00000000000..c03e6cd655c
--- /dev/null
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0006_OpenGenericHandlerWithConcreteMessage_ReportsInfo.md
@@ -0,0 +1,28 @@
+# MO0006_OpenGenericHandlerWithConcreteMessage_ReportsInfo
+
+```json
+[
+ {
+ "Id": "MO0001",
+ "Title": "Missing handler for message type",
+ "Severity": "Warning",
+ "WarningLevel": 1,
+ "Location": ": (4,14)-(4,23)",
+ "MessageFormat": "Message type '{0}' has no registered handler",
+ "Message": "Message type 'global::TestApp.MyCommand' has no registered handler",
+ "Category": "Mediator",
+ "CustomTags": []
+ },
+ {
+ "Id": "MO0006",
+ "Title": "Open generic handler cannot be auto-registered",
+ "Severity": "Info",
+ "WarningLevel": 1,
+ "Location": ": (6,13)-(6,27)",
+ "MessageFormat": "Handler '{0}' is an open generic and cannot be auto-registered",
+ "Message": "Handler 'global::TestApp.GenericHandler' is an open generic and cannot be auto-registered",
+ "Category": "Mediator",
+ "CustomTags": []
+ }
+]
+```
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md
index 58fa8e23493..3300993b0e0 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_CommandWithHandler_NoDiagnostic.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md
index 4f61ee3eb4f..b381e279f49 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_SingleHandlerInterface_NoDiagnostic.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.SomethingHappened),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.SomethingHappenedHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md
index 7ed08a51542..0424cc62015 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ExplicitModuleNameTests.Generate_ModuleWithOnlyName_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class Test2MediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTest2(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md
index 5ba352e34ae..836371f840b 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_MultipleHandlersSameNamespace_DeterministicOrder_MatchesSnapshot.md
@@ -11,6 +11,20 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.AlphaCommand),
+ typeof(global::TestApp.MidCommand),
+ typeof(global::TestApp.ZetaCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.AlphaHandler),
+ typeof(global::TestApp.MidHandler),
+ typeof(global::TestApp.ZetaHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md
index 0fe2f8de85e..d029f60e1dd 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/GenericHandlerTests.Generate_OpenGenericCommand_MatchesSnapshot.md
@@ -13,6 +13,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ProcessCommand),
+ typeof(string),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.StringProcessor),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md
index 815a1841400..7604802cfff 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/InternalHandlerTests.Generate_InternalHandler_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTest(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md
index c3658a948c2..c3630c12c2c 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandOfTResolution_ICommandGeneric_MatchesSnapshot.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ComputeCommand),
+ typeof(long),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ComputeHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md
index ceb0ea09039..826889fce74 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_CommandVoidResolution_ICommandInterface_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.FireAndForgetCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.FireAndForgetHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md
index d73bd07dc3c..da11e07108e 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/KnownTypeSymbolsTests.Generate_WithAllHandlerTypes_AllSymbolsResolved_MatchesSnapshot.md
@@ -11,6 +11,24 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.MyEvent),
+ typeof(global::TestApp.MyQuery),
+ typeof(global::TestApp.ResponseCommand),
+ typeof(global::TestApp.VoidCommand),
+ typeof(int),
+ typeof(string),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.MyEventHandler),
+ typeof(global::TestApp.MyQueryHandler),
+ typeof(global::TestApp.ResponseCommandHandler),
+ typeof(global::TestApp.VoidCommandHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md
index 72c045f859b..24e2ed2e22b 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DefaultAssemblyName_PrefixesWithLastSegment.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetItemQuery),
+ typeof(string),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetItemHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md
index e6fcf81e6b8..68e10a15ecf 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_DottedAssemblyName_UsesLastSegment.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class OrderingMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.PingCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.PingHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddOrdering(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md
index 3d9cdda9749..b640db21259 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MediatorModuleTests.Generate_ModuleFile_ContainsHandlerRegistrations.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateInvoiceCommand),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateInvoiceHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingBatchHandlerGeneratorTests.Generate_BatchEventHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingBatchHandlerGeneratorTests.Generate_BatchEventHandler_MatchesSnapshot.md
index d4efcff8bd3..ba5e349d505 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingBatchHandlerGeneratorTests.Generate_BatchEventHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingBatchHandlerGeneratorTests.Generate_BatchEventHandler_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.BulkOrderHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingConsumerGeneratorTests.Generate_SingleConsumer_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingConsumerGeneratorTests.Generate_SingleConsumer_MatchesSnapshot.md
index 993e7a06d99..4c6a405fe59 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingConsumerGeneratorTests.Generate_SingleConsumer_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingConsumerGeneratorTests.Generate_SingleConsumer_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.AuditLogConsumer),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingDiagnosticTests.MO0011_DuplicateRequestHandler_ReportsError.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingDiagnosticTests.MO0011_DuplicateRequestHandler_ReportsError.md
index 5f2a077c15a..45850dd67d8 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingDiagnosticTests.MO0011_DuplicateRequestHandler_ReportsError.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingDiagnosticTests.MO0011_DuplicateRequestHandler_ReportsError.md
@@ -13,6 +13,13 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetOrderHandlerA),
+ typeof(global::TestApp.GetOrderHandlerB),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_MultipleEventHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_MultipleEventHandlers_MatchesSnapshot.md
index 8409c2d5b30..2d1c84c1ed0 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_MultipleEventHandlers_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_MultipleEventHandlers_MatchesSnapshot.md
@@ -11,6 +11,13 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderPlacedAuditHandler),
+ typeof(global::TestApp.OrderPlacedHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_SingleEventHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_SingleEventHandler_MatchesSnapshot.md
index 439c0cea178..974bc318bad 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_SingleEventHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingEventHandlerGeneratorTests.Generate_SingleEventHandler_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderPlacedHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMixedHandlerTests.Generate_AllHandlerKinds_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMixedHandlerTests.Generate_AllHandlerKinds_MatchesSnapshot.md
index 00d25838b81..2d3b9bc0d83 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMixedHandlerTests.Generate_AllHandlerKinds_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMixedHandlerTests.Generate_AllHandlerKinds_MatchesSnapshot.md
@@ -11,10 +11,32 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ SagaTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderFulfillmentSaga),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.AuditLogConsumer),
+ typeof(global::TestApp.BulkOrderHandler),
+ typeof(global::TestApp.GetOrderStatusHandler),
+ typeof(global::TestApp.OrderPlacedHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
+ // --- Saga Configuration ---
+ global::Mocha.MessageBusHostBuilderExtensions.AddSagaConfiguration<
+ global::TestApp.OrderFulfillmentSaga>(
+ builder,
+ new global::Mocha.MessagingSagaConfiguration
+ {
+ SagaType = typeof(global::TestApp.OrderFulfillmentSaga),
+ });
+
// --- Batch Handlers ---
global::Mocha.MessageBusHostBuilderExtensions.AddHandlerConfiguration(builder,
new global::Mocha.MessagingHandlerConfiguration
@@ -47,10 +69,6 @@ namespace Microsoft.Extensions.DependencyInjection
Factory = global::Mocha.ConsumerFactory.Subscribe()
});
- // --- Sagas ---
- global::Mocha.MessageBusHostBuilderExtensions.AddSaga<
- global::TestApp.OrderFulfillmentSaga>(builder);
-
return builder;
}
}
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_DefaultModuleName_UsesAssemblyName_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_DefaultModuleName_UsesAssemblyName_MatchesSnapshot.md
index 64c064de4d6..989bc735c9a 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_DefaultModuleName_UsesAssemblyName_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_DefaultModuleName_UsesAssemblyName_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class ApiMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderPlacedHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddApi(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_ExplicitModuleName_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_ExplicitModuleName_MatchesSnapshot.md
index 30d6d2e12dc..900f4b07f8f 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_ExplicitModuleName_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingModuleTests.Generate_ExplicitModuleName_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class OrderServiceMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderPlacedHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddOrderService(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMultiInterfaceTests.Generate_HandlerWithBatchAndEvent_RegistersAsBatch_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMultiInterfaceTests.Generate_HandlerWithBatchAndEvent_RegistersAsBatch_MatchesSnapshot.md
index 2e6e525c6d1..7832a2d4859 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMultiInterfaceTests.Generate_HandlerWithBatchAndEvent_RegistersAsBatch_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingMultiInterfaceTests.Generate_HandlerWithBatchAndEvent_RegistersAsBatch_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderBatchAndEventHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_RequestResponseHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_RequestResponseHandler_MatchesSnapshot.md
index 22a5f744550..b6668ab7663 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_RequestResponseHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_RequestResponseHandler_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetOrderStatusHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_SendHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_SendHandler_MatchesSnapshot.md
index cfb11c3f857..76c7f18cf41 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_SendHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingRequestHandlerGeneratorTests.Generate_SendHandler_MatchesSnapshot.md
@@ -11,6 +11,12 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ProcessOrderHandler),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingSagaGeneratorTests.Generate_SimpleSaga_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingSagaGeneratorTests.Generate_SimpleSaga_MatchesSnapshot.md
index de6480149f4..708f31290ed 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingSagaGeneratorTests.Generate_SimpleSaga_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MessagingSagaGeneratorTests.Generate_SimpleSaga_MatchesSnapshot.md
@@ -11,13 +11,24 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMessageBusBuilderExtensions
{
+ [global::Mocha.MessagingModuleInfo(
+ SagaTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderFulfillmentSaga),
+ }
+ )]
public static global::Mocha.IMessageBusHostBuilder AddTests(
this global::Mocha.IMessageBusHostBuilder builder)
{
- // --- Sagas ---
- global::Mocha.MessageBusHostBuilderExtensions.AddSaga<
- global::TestApp.OrderFulfillmentSaga>(builder);
+ // --- Saga Configuration ---
+ global::Mocha.MessageBusHostBuilderExtensions.AddSagaConfiguration<
+ global::TestApp.OrderFulfillmentSaga>(
+ builder,
+ new global::Mocha.MessagingSagaConfiguration
+ {
+ SagaType = typeof(global::TestApp.OrderFulfillmentSaga),
+ });
return builder;
}
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md
index bf2d65a1323..736c127badd 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_AllHandlerTypes_MatchesSnapshot.md
@@ -11,6 +11,25 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderCommand),
+ typeof(global::TestApp.DeleteOrderCommand),
+ typeof(global::TestApp.GetUserQuery),
+ typeof(global::TestApp.OrderCreated),
+ typeof(global::TestApp.UserDto),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderHandler),
+ typeof(global::TestApp.DeleteOrderHandler),
+ typeof(global::TestApp.GetUserHandler),
+ typeof(global::TestApp.OrderCreatedEmailHandler),
+ typeof(global::TestApp.OrderCreatedStatsHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md
index 64c2a35c3fc..e6618a74c62 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/MixedHandlerGeneratorTests.Generate_HandlersInDifferentNamespaces_MatchesSnapshot.md
@@ -11,6 +11,20 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.Orders.CreateOrderCommand),
+ typeof(global::TestApp.Users.GetUserQuery),
+ typeof(global::TestApp.Users.UserDto),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.Orders.CreateOrderHandler),
+ typeof(global::TestApp.Users.GetUserHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md
index 0cbb349af3e..3b7b27831b7 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_AssemblyNameWithHyphen_UsesLastSegmentSanitized_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class Order_ProcessingMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.PingCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.PingHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddOrder_Processing(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md
index e1d6e37457a..1103d90680e 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/ModuleNameHelperTests.Generate_NullAssemblyName_UsesAssemblyDefault_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class UnknownMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.PingCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.PingHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddUnknown(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md
index 212d88fc672..326ce16300c 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NestedHandlerTests.Generate_NestedClassHandler_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.DeleteOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.Outer.DeleteOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md
index 1412f566583..9e81d700556 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_MultipleHandlersForSameNotification_MatchesSnapshot.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderCreated),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.SendEmailHandler),
+ typeof(global::TestApp.UpdateStatsHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md
index 54e548c8e27..c7380066636 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/NotificationHandlerGeneratorTests.Generate_SingleNotificationHandler_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderCreated),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.OrderCreatedEmailHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md
index c64aed506ac..d1d41a97519 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialClassHandler_MatchesSnapshot.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderCommand),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md
index 294e0e41b81..c70fd5b3038 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialQueryHandler_AcrossFiles_MatchesSnapshot.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetOrderQuery),
+ typeof(string),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetOrderQueryHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md
index f68357c1540..8aa2d252d7a 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/PartialClassHandlerTests.Generate_PartialVoidCommandHandler_AcrossFiles_MatchesSnapshot.md
@@ -11,6 +11,16 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ProcessOrderCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ProcessOrderHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md
index bc05067d0f0..c532c40d4c2 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_MultipleQueryHandlers_MatchesSnapshot.md
@@ -11,6 +11,20 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetOrderQuery),
+ typeof(global::TestApp.GetUserQuery),
+ typeof(global::TestApp.OrderDto),
+ typeof(global::TestApp.UserDto),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetOrderHandler),
+ typeof(global::TestApp.GetUserHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md
index d9e1649ad9b..802fb0c0435 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/QueryHandlerGeneratorTests.Generate_QueryHandler_MatchesSnapshot.md
@@ -11,6 +11,17 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetUserQuery),
+ typeof(global::TestApp.UserDto),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.GetUserHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md
index d94e843cedf..ef9d8d1b177 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/WarmUpGeneratorTests.Generate_WarmUpMethod_WithAllHandlerTypes_MatchesSnapshot.md
@@ -11,6 +11,24 @@ namespace Microsoft.Extensions.DependencyInjection
[global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
public static class TestsMediatorBuilderExtensions
{
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateItemCommand),
+ typeof(global::TestApp.DeleteItemCommand),
+ typeof(global::TestApp.GetItemQuery),
+ typeof(global::TestApp.ItemCreated),
+ typeof(global::TestApp.ItemDto),
+ typeof(int),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.CreateItemHandler),
+ typeof(global::TestApp.DeleteItemHandler),
+ typeof(global::TestApp.GetItemHandler),
+ typeof(global::TestApp.ItemCreatedHandler),
+ }
+ )]
public static global::Mocha.Mediator.IMediatorHostBuilder AddTests(
this global::Mocha.Mediator.IMediatorHostBuilder builder)
{
diff --git a/src/Mocha/test/Mocha.Sagas.TestHelpers/TestMessageBus.cs b/src/Mocha/test/Mocha.Sagas.TestHelpers/TestMessageBus.cs
index 4bbe9ef60d3..d10ee4493c6 100644
--- a/src/Mocha/test/Mocha.Sagas.TestHelpers/TestMessageBus.cs
+++ b/src/Mocha/test/Mocha.Sagas.TestHelpers/TestMessageBus.cs
@@ -9,26 +9,26 @@ public sealed class TestMessageBus(TestMessageOutbox outbox) : IMessageBus
public List CancelledTokens { get; } = [];
- public ValueTask PublishAsync(T message, CancellationToken cancellationToken)
+ public ValueTask PublishAsync(T message, CancellationToken cancellationToken) where T : notnull
{
- outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Publish, message!, null));
+ outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Publish, message, null));
return ValueTask.CompletedTask;
}
public ValueTask PublishAsync(T message, PublishOptions options, CancellationToken cancellationToken)
+ where T : notnull
{
- outbox.Messages.Add(
- new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Publish, message!, options));
+ outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Publish, message, options));
return ValueTask.CompletedTask;
}
- public ValueTask SendAsync(object message, CancellationToken cancellationToken)
+ public ValueTask SendAsync(T message, CancellationToken cancellationToken) where T : notnull
{
outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Send, message, null));
return ValueTask.CompletedTask;
}
- public ValueTask SendAsync(object message, SendOptions options, CancellationToken cancellationToken)
+ public ValueTask SendAsync(T message, SendOptions options, CancellationToken cancellationToken) where T : notnull
{
outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Send, message, options));
return ValueTask.CompletedTask;
@@ -104,10 +104,11 @@ public ValueTask SchedulePublishAsync(
});
}
- public ValueTask ScheduleSendAsync(
- object message,
+ public ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
CancellationToken cancellationToken)
+ where T : notnull
{
var token = $"test:{Interlocked.Increment(ref _scheduleCounter)}";
outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Send, message, null));
@@ -120,11 +121,12 @@ public ValueTask ScheduleSendAsync(
});
}
- public ValueTask ScheduleSendAsync(
- object message,
+ public ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
SendOptions options,
CancellationToken cancellationToken)
+ where T : notnull
{
var token = $"test:{Interlocked.Increment(ref _scheduleCounter)}";
outbox.Messages.Add(new TestMessageOutbox.Operation(TestMessageOutbox.OperationKind.Send, message, options));
diff --git a/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs b/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs
index 4d9cae582ef..1381344dc76 100644
--- a/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs
+++ b/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs
@@ -537,6 +537,7 @@ private sealed class StubTransportOptions : IReadOnlyTransportOptions
private sealed class StubMessagingOptions : IReadOnlyMessagingOptions
{
public MessageContentType DefaultContentType => new("application/json");
+ public bool IsAotCompatible => false;
}
private sealed class StubHostInfo : IHostInfo
diff --git a/src/Mocha/test/Mocha.Tests/Scheduling/MessageBusSchedulingExtensionsTests.cs b/src/Mocha/test/Mocha.Tests/Scheduling/MessageBusSchedulingExtensionsTests.cs
index 54f055d4344..ca0ff8dc11b 100644
--- a/src/Mocha/test/Mocha.Tests/Scheduling/MessageBusSchedulingExtensionsTests.cs
+++ b/src/Mocha/test/Mocha.Tests/Scheduling/MessageBusSchedulingExtensionsTests.cs
@@ -47,16 +47,16 @@ private sealed class SpyMessageBus : IMessageBus
public List<(object Message, DateTimeOffset ScheduledTime)> ScheduledSendMessages { get; } = [];
public List<(object Message, DateTimeOffset ScheduledTime)> ScheduledPublishMessages { get; } = [];
- public ValueTask SendAsync(object message, CancellationToken cancellationToken) =>
+ public ValueTask SendAsync(T message, CancellationToken cancellationToken) where T : notnull =>
ValueTask.CompletedTask;
- public ValueTask SendAsync(object message, SendOptions options, CancellationToken cancellationToken) =>
+ public ValueTask SendAsync(T message, SendOptions options, CancellationToken cancellationToken) where T : notnull =>
ValueTask.CompletedTask;
- public ValueTask PublishAsync(T message, CancellationToken cancellationToken) =>
+ public ValueTask PublishAsync(T message, CancellationToken cancellationToken) where T : notnull =>
ValueTask.CompletedTask;
- public ValueTask PublishAsync(T message, PublishOptions options, CancellationToken cancellationToken) =>
+ public ValueTask PublishAsync(T message, PublishOptions options, CancellationToken cancellationToken) where T : notnull =>
ValueTask.CompletedTask;
public ValueTask RequestAsync(
@@ -101,20 +101,20 @@ public ValueTask SchedulePublishAsync(
return ValueTask.FromResult(new SchedulingResult { ScheduledTime = scheduledTime });
}
- public ValueTask ScheduleSendAsync(
- object message,
+ public ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken) where T : notnull
{
ScheduledSendMessages.Add((message, scheduledTime));
return ValueTask.FromResult(new SchedulingResult { ScheduledTime = scheduledTime });
}
- public ValueTask ScheduleSendAsync(
- object message,
+ public ValueTask ScheduleSendAsync(
+ T message,
DateTimeOffset scheduledTime,
SendOptions options,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken) where T : notnull
{
ScheduledSendMessages.Add((message, scheduledTime));
return ValueTask.FromResult(new SchedulingResult { ScheduledTime = scheduledTime });
diff --git a/website/src/docs/mocha/v1/diagnostics.md b/website/src/docs/mocha/v1/diagnostics.md
index 782e39e1468..7f1eee8e5f7 100644
--- a/website/src/docs/mocha/v1/diagnostics.md
+++ b/website/src/docs/mocha/v1/diagnostics.md
@@ -20,6 +20,10 @@ Mocha uses a Roslyn source generator to validate your message handlers, consumer
| [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 |
+| [MO0015](#mo0015) | Missing JsonSerializerContext for AOT | Error |
+| [MO0016](#mo0016) | Missing JsonSerializable attribute | Error |
+| [MO0018](#mo0018) | Type not in JsonSerializerContext | Warning |
+| [MO0020](#mo0020) | Command/query sent but no handler found | Warning |
# Mediator diagnostics
@@ -466,3 +470,211 @@ public class RefundSaga : Saga
}
}
```
+
+## MO0015
+
+**Missing JsonSerializerContext for AOT**
+
+| | |
+| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Severity** | Error |
+| **Message** | `MessagingModule '{0}' must specify JsonContext when publishing for AOT. Add JsonContext = typeof(YourJsonContext) to the MessagingModule attribute.` |
+
+### Cause
+
+The project has `PublishAot` set to `true` but the `[assembly: MessagingModule]` attribute does not include a `JsonContext` property. AOT publishing requires a `JsonSerializerContext` so the source generator can produce trim-safe serialization code for all message types.
+
+### Example
+
+```csharp
+using Mocha;
+
+// No JsonContext specified while targeting AOT - triggers MO0015
+[assembly: MessagingModule("OrderService")]
+```
+
+### Fix
+
+Create a `JsonSerializerContext` that includes all your message types and reference it from the `MessagingModule` attribute.
+
+```csharp
+using System.Text.Json.Serialization;
+using Mocha;
+
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+
+public record OrderPlaced(Guid OrderId);
+
+[JsonSerializable(typeof(OrderPlaced))]
+public partial class OrderServiceJsonContext : JsonSerializerContext;
+```
+
+## MO0016
+
+**Missing JsonSerializable attribute**
+
+| | |
+| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Severity** | Error |
+| **Message** | `Type '{0}' is used as a message type but is not included in JsonSerializerContext '{1}'. Add [JsonSerializable(typeof({0}))] to the context.` |
+
+### Cause
+
+A type is used as a message, request, response, or saga state through a handler registration, but it is not declared via `[JsonSerializable(typeof(...))]` on the `JsonSerializerContext` specified in the `MessagingModule` attribute. Without this declaration, the AOT compiler cannot generate serialization code for the type.
+
+### Example
+
+```csharp
+using System.Text.Json.Serialization;
+using Mocha;
+
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+
+public record OrderPlaced(Guid OrderId);
+
+public class OrderPlacedHandler : IEventHandler
+{
+ public ValueTask HandleAsync(
+ OrderPlaced message,
+ CancellationToken ct)
+ => ValueTask.CompletedTask;
+}
+
+// OrderPlaced is missing from the context - triggers MO0016
+[JsonSerializable(typeof(string))]
+public partial class OrderServiceJsonContext : JsonSerializerContext;
+```
+
+### Fix
+
+Add a `[JsonSerializable]` attribute for every message type used by your handlers.
+
+```csharp
+using System.Text.Json.Serialization;
+using Mocha;
+
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+
+public record OrderPlaced(Guid OrderId);
+
+public class OrderPlacedHandler : IEventHandler
+{
+ public ValueTask HandleAsync(
+ OrderPlaced message,
+ CancellationToken ct)
+ => ValueTask.CompletedTask;
+}
+
+[JsonSerializable(typeof(OrderPlaced))]
+public partial class OrderServiceJsonContext : JsonSerializerContext;
+```
+
+## MO0018
+
+**Type not in JsonSerializerContext**
+
+| | |
+| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
+| **Severity** | Warning |
+| **Message** | `Type '{0}' is used in a {1} call but is not included in JsonSerializerContext '{2}'. Add [JsonSerializable(typeof({0}))] to the context.` |
+
+### Cause
+
+AOT publishing is enabled and a message type used at a call site (for example `bus.PublishAsync()`) is not declared in the `JsonSerializerContext`. This is similar to [MO0016](#mo0016), but applies to types discovered at call sites rather than handler registrations. Without the declaration, the message cannot be serialized at runtime in an AOT environment.
+
+### Example
+
+```csharp
+using System.Text.Json.Serialization;
+using Mocha;
+
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+
+public record OrderPlaced(Guid OrderId);
+public record OrderShipped(Guid OrderId);
+
+// OrderShipped is published but missing from the context - triggers MO0018
+public class OrderService(IMessageBus bus)
+{
+ public async Task ShipOrderAsync(Guid orderId, CancellationToken ct)
+ {
+ await bus.PublishAsync(new OrderShipped(orderId), ct);
+ }
+}
+
+[JsonSerializable(typeof(OrderPlaced))]
+public partial class OrderServiceJsonContext : JsonSerializerContext;
+```
+
+### Fix
+
+Add a `[JsonSerializable]` attribute for every type used at a call site.
+
+```csharp
+using System.Text.Json.Serialization;
+using Mocha;
+
+[assembly: MessagingModule("OrderService", JsonContext = typeof(OrderServiceJsonContext))]
+
+public record OrderPlaced(Guid OrderId);
+public record OrderShipped(Guid OrderId);
+
+[JsonSerializable(typeof(OrderPlaced))]
+[JsonSerializable(typeof(OrderShipped))]
+public partial class OrderServiceJsonContext : JsonSerializerContext;
+```
+
+# Mediator call-site diagnostics
+
+These diagnostics are reported when the source generator inspects call sites that use `ISender` to dispatch commands or queries.
+
+## MO0020
+
+**Command/query sent but no handler found**
+
+| | |
+| ------------ | ------------------------------------------------------------------------------------------------------- |
+| **Severity** | Warning |
+| **Message** | `Type '{0}' is sent via {1} but no handler was found in this assembly. Ensure a handler is registered.` |
+
+### Cause
+
+A command or query type is dispatched via `ISender.SendAsync` or `ISender.QueryAsync`, but no corresponding handler implementation exists in the assembly. This catches cases where a call site references a message type that was never wired up with a handler.
+
+### Example
+
+```csharp
+using Mocha.Mediator;
+
+public record PlaceOrder(Guid OrderId, decimal Total) : ICommand;
+
+// PlaceOrder is sent but no handler exists - triggers MO0020
+public class OrderController(ISender sender)
+{
+ public async Task CreateOrderAsync(Guid orderId, CancellationToken ct)
+ {
+ await sender.SendAsync(new PlaceOrder(orderId, 99.99m), ct);
+ }
+}
+```
+
+### Fix
+
+Implement a handler for the message type, or remove the call site if the handler is intentionally in another assembly.
+
+```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;
+ }
+}
+```