diff --git a/.codex b/.codex
new file mode 100644
index 00000000000..e69de29bb2d
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..2c8a9712421
--- /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..ee2a12c362f
--- /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..28b2bb33da7
--- /dev/null
+++ b/src/Mocha/examples/AotExample/AotExample.OrderService/OrderSimulatorWorker.cs
@@ -0,0 +1,98 @@
+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..813350dfb1e
--- /dev/null
+++ b/src/Mocha/examples/AotExample/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+ 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/Examples/Observability/OpenTelemetry/OpenTelemetryExample.cs b/src/Mocha/src/Examples/Observability/OpenTelemetry/OpenTelemetryExample.cs
index fda91d42cbb..b44b5686ff0 100644
--- a/src/Mocha/src/Examples/Observability/OpenTelemetry/OpenTelemetryExample.cs
+++ b/src/Mocha/src/Examples/Observability/OpenTelemetry/OpenTelemetryExample.cs
@@ -39,7 +39,7 @@
// Spans are emitted to the "Mocha" activity source - subscribe via AddSource("Mocha").
.AddInstrumentation()
// Register a custom listener alongside the built-in one for application-level telemetry.
- // Multiple listeners compose automatically — no manual aggregation needed.
+ // Multiple listeners compose automatically - no manual aggregation needed.
.AddDiagnosticEventListener()
.AddEventHandler()
.AddInMemory();
@@ -91,7 +91,7 @@ public ValueTask HandleAsync(
// --- Custom diagnostic listener ---
// Extend MessagingDiagnosticEventListener to collect telemetry or integrate with a
-// non-OpenTelemetry backend. Override only the methods you care about — the base
+// non-OpenTelemetry backend. Override only the methods you care about - the base
// class provides no-op defaults for the rest. Each scope method returns an
// IDisposable whose disposal marks the end of the observed scope.
public sealed class ConsoleDiagnosticObserver : MessagingDiagnosticEventListener
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 ad7f4fa0fdf..53d56ad1d15 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..980a38f5933 100644
--- a/src/Mocha/src/Mocha.Analyzers/Errors.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Errors.cs
@@ -84,6 +84,24 @@ public static class Errors
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ ///
+ /// Gets the descriptor for MO0006: a mediator handler is declared with unbound type parameters.
+ ///
+ ///
+ /// Reported as an info on the handler declaration when it has unbound type parameters.
+ /// Source-generated registration is skipped for open generic handlers because there is no
+ /// closed type to register. Manual registration of a closed form (for example
+ /// AddHandler<ConcreteHandler<MyType>>()) remains supported; suppress the
+ /// diagnostic on the declaration if the handler is registered that way intentionally.
+ ///
+ public static readonly DiagnosticDescriptor OpenGenericHandler = new(
+ id: "MO0006",
+ title: "Open generic handler cannot be auto-registered",
+ messageFormat: "Handler '{0}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ category: "Mediator",
+ defaultSeverity: DiagnosticSeverity.Info,
+ isEnabledByDefault: true);
+
///
/// Gets the descriptor for MO0011: a request type has more than one handler.
///
@@ -143,4 +161,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..e4b4ec385ab 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,115 @@ 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 (including framework base types
+ /// such as IEventRequest and closed IEventRequest<T>), or
+ /// to omit.
+ ///
+ public void WriteMessageConfiguration(
+ string messageTypeName,
+ string jsonContextTypeName,
+ IReadOnlyList? 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 = global::System.Collections.Immutable.ImmutableArray.Create(");
+ Writer.IncreaseIndent();
+ for (var i = 0; i < enclosedTypes.Count; i++)
+ {
+ var typeName = enclosedTypes[i];
+ var isLast = i == enclosedTypes.Count - 1;
+ Writer.WriteIndentedLine(isLast ? "typeof({0})" : "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 +231,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 +254,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..1f62ac16bed
--- /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..896cc1abcc9
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Filters/InvocationModuleFilter.cs
@@ -0,0 +1,29 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Mocha.Analyzers.Filters;
+
+///
+/// Provides a singleton that matches invocation expressions
+/// where the non-generic method name starts with "Add" and the invocation has no arguments.
+/// 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
+ {
+ ArgumentList.Arguments.Count: 0,
+ Expression: MemberAccessExpressionSyntax { Name: IdentifierNameSyntax identifier }
+ }
+ && identifier.Identifier.Text.StartsWith("Add", StringComparison.Ordinal);
+
+ ///
+ /// Gets the singleton instance of .
+ ///
+ public static InvocationModuleFilter Instance { get; } = new();
+}
diff --git a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs
index 197cff1c0c9..70c15829f21 100644
--- a/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs
+++ b/src/Mocha/src/Mocha.Analyzers/Generators/DependencyInjectionGenerator.cs
@@ -35,6 +35,30 @@ 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);
+ }
+
+ 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 +69,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..cd4e6c050bb 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,235 @@ 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;
+ var isAotPublish = false;
+
+ foreach (var info in syntaxInfos)
+ {
+ if (jsonContextTypeName is null && info is MessagingModuleInfo moduleInfo)
+ {
+ jsonContextTypeName = moduleInfo.JsonContextTypeName;
+ }
+
+ if (info is AotPublishInfo aotPublishInfo)
+ {
+ isAotPublish = aotPublishInfo.IsAotPublish;
+ }
+ }
+
+ // Collect type names imported from module methods invoked in this compilation.
+ // Those module methods have already registered their own message, saga, and handler metadata.
+ 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 and not already registered by invoked modules qualify.
+ 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");
+ if (isAotPublish)
+ {
+ builder.WriteStrictModeConfiguration();
+ }
+
+ builder.WriteJsonTypeInfoResolverRegistration(jsonContextTypeName);
+
+ // Framework base types are emitted as typeof(...) so MessageType.Complete
+ // can resolve their URN through the runtime naming convention.
+ 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 };
+
+ // Walk the hierarchy; include registered user types and framework base types.
+ if (hierarchyLookup.TryGetValue(messageTypeName, out var hierarchy))
+ {
+ foreach (var typeName in hierarchy)
+ {
+ if (enclosed.Contains(typeName))
+ {
+ continue;
+ }
+
+ if (messageTypes.Contains(typeName) || IsFrameworkBaseType(typeName))
+ {
+ enclosed.Add(typeName);
+ }
+ }
+ }
+
+ // Precompute specificity scores; List.Sort reorders during comparison,
+ // so scoring inside the comparator produces non-deterministic results.
+ var scores = new Dictionary(enclosed.Count, StringComparer.Ordinal);
+
+ foreach (var typeName in enclosed)
+ {
+ if (!hierarchyLookup.TryGetValue(typeName, out var h))
+ {
+ scores[typeName] = 0;
+ continue;
+ }
+
+ var score = 0;
+ foreach (var other in enclosed)
+ {
+ if (other != typeName && h.Contains(other))
+ {
+ score++;
+ }
+ }
+ scores[typeName] = score;
+ }
+
+ enclosed.Sort((a, b) => scores[b].CompareTo(scores[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)
+ {
+ var sagaJsonContextTypeName = jsonContextTypeNames.Contains(saga.StateTypeName)
+ ? jsonContextTypeName
+ : null;
+
+ builder.WriteSagaConfiguration(saga.SagaTypeName, saga.StateTypeName, sagaJsonContextTypeName);
+ }
+ }
var batchHandlers = handlers
.Where(h => h.Kind == MessagingHandlerKind.Batch)
@@ -103,20 +322,26 @@ public void Generate(
}
}
- if (sagas.Count > 0)
- {
- builder.WriteSectionComment("Sagas");
-
- foreach (var saga in sagas)
- {
- builder.WriteSagaRegistration(saga);
- }
- }
-
builder.WriteEndRegistrationMethod();
builder.WriteEndClass();
builder.WriteEndNamespace();
addSource(builder.HintName + ".g.cs", builder.ToSourceText());
}
+
+ ///
+ /// Determines whether the fully qualified display string refers to a framework base type
+ /// (Mocha.IEventRequest or closed Mocha.IEventRequest<T>) that must
+ /// flow into EnclosedTypes without being registered as a standalone message type.
+ ///
+ private static bool IsFrameworkBaseType(string fullyQualifiedName)
+ {
+ if (fullyQualifiedName == SyntaxConstants.IEventRequestDisplay)
+ {
+ return true;
+ }
+
+ return fullyQualifiedName.StartsWith(SyntaxConstants.IEventRequestOfTDisplayPrefix, StringComparison.Ordinal)
+ && fullyQualifiedName.EndsWith(">", StringComparison.Ordinal);
+ }
}
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..734814a226d 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,34 @@ 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)
+ || (
+ knownSymbols.INotificationHandler is not null
+ && namedTypeSymbol.FindImplementedInterface(knownSymbols.INotificationHandler) 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..632d15d1ac2
--- /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;
+ }
+
+ var 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..dcc1bd83bbb
--- /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 diagnostics and generators use this to treat types registered by invoked
+/// module methods as already covered.
+///
+///
+/// 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;
+ }
+
+ var 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..5f45c78feb1 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 = [];
+
+ 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..468a0c7d889 100644
--- a/src/Mocha/src/Mocha.Analyzers/MessagingGenerator.cs
+++ b/src/Mocha/src/Mocha.Analyzers/MessagingGenerator.cs
@@ -2,6 +2,7 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Mocha.Analyzers.Filters;
using Mocha.Analyzers.Generators;
@@ -25,14 +26,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 +71,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 +114,51 @@ 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);
+ });
+
+ var jsonSerializableInfos = context
+ .SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (s, _) => IsJsonSerializableAttributeCandidate(s),
+ transform: static (ctx, ct) => ExtractJsonSerializableInfo(ctx.Node, ctx.SemanticModel, ct))
+ .WhereNotNull()
+ .WithComparer(EqualityComparer.Default)
+ .Collect()
+ .WithTrackingName("MochaJsonSerializableInfos");
+
+ // Keep JsonContext data syntax-driven instead of flowing CompilationProvider into this path.
+ var jsonContextInfo = syntaxInfos
+ .Combine(jsonSerializableInfos)
+ .Select(static (source, _) => BuildJsonContextInfo(source.Left, source.Right));
+
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 +184,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 +235,41 @@ 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, isAotPublish, jsonContextInfo);
+
+ // Extract context-only message types from JsonSerializerContext (types without handlers).
+ var augmentedInfos = ExtractContextOnlyTypes(syntaxInfos, jsonContextInfo);
+ augmentedInfos = augmentedInfos.Add(new AotPublishInfo(isAotPublish));
+
+ // 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
+ // registrations for types already covered by module methods invoked in this compilation.
+ 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 +295,131 @@ 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 bool IsJsonSerializableAttributeCandidate(SyntaxNode node)
+ => node is AttributeSyntax attribute
+ && GetUnqualifiedName(attribute.Name) is "JsonSerializable" or "JsonSerializableAttribute";
+
+ private static string? GetUnqualifiedName(NameSyntax name)
+ => name switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.Text,
+ GenericNameSyntax generic => generic.Identifier.Text,
+ QualifiedNameSyntax qualified => GetUnqualifiedName(qualified.Right),
+ AliasQualifiedNameSyntax aliasQualified => aliasQualified.Name.Identifier.Text,
+ _ => null
+ };
+
+ private static JsonSerializableAttributeInfo? ExtractJsonSerializableInfo(
+ SyntaxNode node,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken)
+ {
+ if (node is not AttributeSyntax attribute
+ || attribute.ArgumentList?.Arguments.FirstOrDefault()?.Expression is not TypeOfExpressionSyntax typeOf)
+ {
+ return null;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var contextDeclaration = attribute.FirstAncestorOrSelf();
+ if (contextDeclaration is null)
+ {
+ return null;
+ }
+
+ var contextSymbol = semanticModel.GetDeclaredSymbol(contextDeclaration, cancellationToken);
+ if (contextSymbol is null)
+ {
+ return null;
+ }
+
+ if (semanticModel.GetTypeInfo(typeOf.Type, cancellationToken).Type is not INamedTypeSymbol serializableType)
+ {
+ return null;
+ }
+
+ return new JsonSerializableAttributeInfo(
+ contextSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ CreateJsonSerializableTypeInfo(serializableType, attribute.GetLocation().ToLocationInfo()));
+ }
+
+ private static JsonContextInfo BuildJsonContextInfo(
+ ImmutableArray syntaxInfos,
+ ImmutableArray jsonSerializableInfos)
+ {
+ // 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 serializableTypes = new List();
+
+ foreach (var info in jsonSerializableInfos)
+ {
+ if (info.JsonContextTypeName == jsonContextTypeName)
+ {
+ serializableTypes.Add(info.SerializableType);
+ }
+ }
+
+ return new JsonContextInfo(
+ jsonContextTypeName,
+ new ImmutableEquatableArray(serializableTypes));
+ }
+
+ private static JsonSerializableTypeInfo CreateJsonSerializableTypeInfo(
+ INamedTypeSymbol serializableType,
+ LocationInfo? location)
+ {
+ 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));
+ }
+
+ return new JsonSerializableTypeInfo(
+ typeName,
+ new ImmutableEquatableArray(hierarchy),
+ location);
+ }
+
+ private sealed record JsonSerializableAttributeInfo(
+ string JsonContextTypeName,
+ JsonSerializableTypeInfo SerializableType);
+
private static void ValidateRequestHandlerPairing(
SourceProductionContext context,
ImmutableArray syntaxInfos)
@@ -226,12 +475,286 @@ private static void ValidateRequestHandlerPairing(
}
}
+ private static void ValidateAotJsonContext(
+ SourceProductionContext context,
+ ImmutableArray syntaxInfos,
+ ImmutableArray callSiteInfos,
+ MessagingModuleInfo moduleInfo,
+ bool isAotPublish,
+ JsonContextInfo jsonContextInfo)
+ {
+ if (!isAotPublish)
+ {
+ 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);
+ }
+ }
+
+ AddRequiredCallSiteTypes(callSiteInfos, requiredTypes);
+
+ // 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 AddRequiredCallSiteTypes(
+ ImmutableArray callSiteInfos,
+ HashSet requiredTypes)
+ {
+ foreach (var info in callSiteInfos)
+ {
+ if (info is CallSiteMessageTypeInfo callSite
+ && callSite.Kind is not (CallSiteKind.MediatorSend or CallSiteKind.MediatorQuery or CallSiteKind.MediatorPublish))
+ {
+ requiredTypes.Add(callSite.MessageTypeName);
+
+ if (callSite.ResponseTypeName is not null)
+ {
+ requiredTypes.Add(callSite.ResponseTypeName);
+ }
+ }
+ }
+ }
+
+ private static void ValidateCallSiteJsonContext(
+ SourceProductionContext context,
+ ImmutableArray callSiteInfos,
+ bool isAotPublish,
+ JsonContextInfo jsonContextInfo)
+ {
+ if (!isAotPublish || 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 and no local context, MO0015 handles the missing context case.
+ if (coveredTypes.Count == 0 && jsonContextInfo.JsonContextTypeName is null)
+ {
+ 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,
+ 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)
@@ -265,6 +788,7 @@ private static Location ReconstructLocation(LocationInfo? locationInfo)
file static class Extensions
{
- public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source)
+ public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source)
+ where T : class
=> source.Where(static t => t is not null)!;
}
diff --git a/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj b/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj
index 6812203aab7..33faa94450b 100644
--- a/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj
+++ b/src/Mocha/src/Mocha.Analyzers/Mocha.Analyzers.csproj
@@ -29,4 +29,8 @@
+
+
+
+
diff --git a/src/Mocha/src/Mocha.Analyzers/Models/AotPublishInfo.cs b/src/Mocha/src/Mocha.Analyzers/Models/AotPublishInfo.cs
new file mode 100644
index 00000000000..d6f83b2e9d7
--- /dev/null
+++ b/src/Mocha/src/Mocha.Analyzers/Models/AotPublishInfo.cs
@@ -0,0 +1,13 @@
+namespace Mocha.Analyzers;
+
+///
+/// Carries the evaluated PublishAot MSBuild property to source generators.
+///
+///
+/// A value indicating whether the current compilation is being published for AOT.
+///
+public sealed record AotPublishInfo(bool IsAotPublish) : SyntaxInfo
+{
+ ///
+ public override string OrderByKey => "AotPublish";
+}
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..890cbb72330
--- /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 and resolver registration are emitted when a `JsonContext` is specified.
+- **`PublishAot` MSBuild property** - controls **strict mode and validation strictness**: when `true`, the generated module enables AOT-compatible runtime mode and diagnostics MO0015/MO0016/MO0018 fire.
+
+Validation diagnostics fire when **`PublishAot` is true**. Serializer code generation requires `JsonContext`.
+
+### 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. **JsonTypeInfoResolver is registered** - the specified `JsonSerializerContext` is added as a resolver.
+
+3. **AOT strict mode is not enabled by JsonContext alone** - `IsAotCompatible = true` is emitted only when `PublishAot` is true.
+
+4. **Validation diagnostics do not fire by JsonContext alone** - MO0015, MO0016, and MO0018 are checked only when `PublishAot` is true.
+
+### 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` |
+| MO0016 | `PublishAot == true` |
+| MO0018 | `PublishAot == true` |
+
+Handlers or sagas that carry a diagnostic (e.g., MO0012, MO0013, MO0014) are **excluded from code generation** - no `AddHandlerConfiguration` or `AddSagaConfiguration` is emitted for them. Only entries with zero diagnostics flow to the generator.
+
+---
+
+## 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..c80bc2c9dc0 100644
--- a/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs
+++ b/src/Mocha/src/Mocha.Analyzers/SyntaxConstants.cs
@@ -91,8 +91,69 @@ public static class SyntaxConstants
///
public const string IEventRequestOfT = "Mocha.IEventRequest`1";
+ ///
+ /// Gets the fully qualified display string for the IEventRequest marker interface,
+ /// used when comparing emitted typeof(...) expressions.
+ ///
+ public const string IEventRequestDisplay = "global::Mocha.IEventRequest";
+
+ ///
+ /// Gets the fully qualified display prefix for a closed IEventRequest<TResponse>,
+ /// used to detect framework base types in emitted typeof(...) expressions.
+ ///
+ public const string IEventRequestOfTDisplayPrefix = "global::Mocha.IEventRequest<";
+
///
/// 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/Configuration/MediatorHandlerConfiguration.cs b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs
index 430e626060c..96ac517e83b 100644
--- a/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs
+++ b/src/Mocha/src/Mocha.Mediator/Configuration/MediatorHandlerConfiguration.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Mocha.Mediator;
///
@@ -14,6 +16,7 @@ public class MediatorHandlerConfiguration : MediatorConfiguration
///
/// Gets or sets the message type (command, query, or notification type).
///
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
public Type? MessageType { get; set; }
///
diff --git a/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs b/src/Mocha/src/Mocha.Mediator/DependencyInjection/MediatorBuilder.cs
index 59144942e17..00cd1c14d82 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,6 +16,14 @@ namespace Mocha.Mediator;
///
public sealed class MediatorBuilder : IMediatorBuilder
{
+ private readonly List _middlewares = [MediatorDiagnosticMiddleware.Create()];
+
+ private readonly List>> _pipelineModifiers = [];
+ private readonly Dictionary> _handlerDescriptors = [];
+ private readonly List> _configureServices = [];
+ private readonly List> _configureFeatures = [];
+ private readonly MediatorOptions _options = new();
+
private static readonly MethodInfo s_buildCommandPipeline =
typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildCommandPipeline))!;
@@ -27,17 +36,6 @@ public sealed class MediatorBuilder : IMediatorBuilder
private static readonly MethodInfo s_buildNotificationPipeline =
typeof(PipelineBuilder).GetMethod(nameof(PipelineBuilder.BuildNotificationPipeline))!;
- private readonly List _middlewares =
- [
- MediatorDiagnosticMiddleware.Create()
- ];
-
- private readonly List>> _pipelineModifiers = [];
- private readonly Dictionary> _handlerDescriptors = [];
- private readonly List> _configureServices = [];
- private readonly List> _configureFeatures = [];
- private readonly MediatorOptions _options = new();
-
///
public IMediatorBuilder ConfigureOptions(Action configure)
{
@@ -106,8 +104,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 +169,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,16 +207,16 @@ 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>();
+ var notificationTerminals = new Dictionary();
foreach (var (handlerType, configureDelegate) in _handlerDescriptors)
{
@@ -223,21 +228,24 @@ public MediatorRuntime Build(IServiceProvider applicationServices)
if (config.Kind == MediatorHandlerKind.Notification)
{
- if (!notificationTerminals.TryGetValue(config.MessageType!, out var list))
+ if (!notificationTerminals.TryGetValue(config.MessageType!, out var notification))
{
- list = [];
- notificationTerminals[config.MessageType!] = list;
+ notification = new NotificationTerminals(config.MessageType!);
+ notificationTerminals[config.MessageType!] = notification;
}
- list.Add(terminal);
+ notification.Terminals.Add(terminal);
}
else
{
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);
}
}
@@ -245,19 +253,20 @@ public MediatorRuntime Build(IServiceProvider applicationServices)
// wrapped in middleware, producing a MediatorDelegate[] per notification type.
var notificationPipelines = new Dictionary>(notificationTerminals.Count);
- foreach (var (notificationType, terminals) in notificationTerminals)
+ foreach (var notification in notificationTerminals.Values)
{
- factoryCtx.MessageType = notificationType;
+ factoryCtx.MessageType = notification.MessageType;
factoryCtx.ResponseType = null;
+ var terminals = notification.Terminals;
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();
+ notificationPipelines[notification.MessageType] = compiled.ToImmutable();
}
var pools = applicationServices.GetRequiredService();
@@ -270,27 +279,27 @@ 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)
{
return config.Kind switch
{
- MediatorHandlerKind.Command =>
- (MediatorDelegate)s_buildCommandPipeline
- .MakeGenericMethod(config.HandlerType!, config.MessageType!)
- .Invoke(null, null)!,
+ MediatorHandlerKind.Command => (MediatorDelegate)
+ s_buildCommandPipeline.MakeGenericMethod(config.HandlerType!, config.MessageType!).Invoke(null, null)!,
- MediatorHandlerKind.CommandResponse =>
- (MediatorDelegate)s_buildCommandResponsePipeline
+ MediatorHandlerKind.CommandResponse => (MediatorDelegate)
+ s_buildCommandResponsePipeline
.MakeGenericMethod(config.HandlerType!, config.MessageType!, config.ResponseType!)
.Invoke(null, null)!,
- MediatorHandlerKind.Query =>
- (MediatorDelegate)s_buildQueryPipeline
+ MediatorHandlerKind.Query => (MediatorDelegate)
+ s_buildQueryPipeline
.MakeGenericMethod(config.HandlerType!, config.MessageType!, config.ResponseType!)
.Invoke(null, null)!,
- MediatorHandlerKind.Notification =>
- (MediatorDelegate)s_buildNotificationPipeline
+ MediatorHandlerKind.Notification => (MediatorDelegate)
+ s_buildNotificationPipeline
.MakeGenericMethod(config.HandlerType!, config.MessageType!)
.Invoke(null, null)!,
@@ -317,4 +326,13 @@ private static void AddDiagnosticEvents(IServiceCollection services)
};
});
}
+
+ private sealed class NotificationTerminals(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type messageType)
+ {
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
+ public Type MessageType { get; } = messageType;
+
+ public List Terminals { get; } = [];
+ }
}
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..25f7dc83e76 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 is only used when source-generated handler configuration is absent.")]
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/MediatorMiddlewareFactoryContext.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs
index 53b47c6439d..346527792bd 100644
--- a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs
+++ b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContext.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Mocha.Mediator;
///
@@ -23,6 +25,7 @@ public sealed class MediatorMiddlewareFactoryContext
///
/// Gets the message type of the pipeline being compiled.
///
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
public Type MessageType { get; internal set; } = null!;
///
diff --git a/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs b/src/Mocha/src/Mocha.Mediator/MediatorMiddlewareFactoryContextExtensions.cs
index 0605707f264..95512b41094 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,7 +60,9 @@ public static bool IsResponseAssignableTo(this MediatorMiddlewareFactoryConte
public static bool IsResponseAssignableTo(this MediatorMiddlewareFactoryContext context, Type type)
=> context.ResponseType is not null && type.IsAssignableFrom(context.ResponseType);
- private static bool HasGenericInterface(Type type, Type openGeneric)
+ private static bool HasGenericInterface(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] 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 04eda7088ef..bde0988c8a5 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 2c190c14e9b..7fc4dc14225 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 existingDelegate = _messageDescriptors.GetValueOrDefault(messageType);
+
+ if (existingDelegate is not null)
{
- return this;
+ 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;
+
+ void Configure(IMessageTypeDescriptor descriptor)
+ {
+ descriptor.AddSerializer(serializer);
+
+ if (!enclosedTypes.IsDefaultOrEmpty)
+ {
+ descriptor.Extend().Configuration.EnclosedTypes = enclosedTypes;
+ }
+ }
+ }
+
+ ///
+ [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;
}
@@ -370,6 +429,11 @@ public MessagingRuntime Build(IServiceProvider applicationServices)
var servicesCollection = new ServiceCollection();
AddCoreServices(servicesCollection, applicationServices);
+ foreach (var configure in _configureServices)
+ {
+ configure(applicationServices, servicesCollection);
+ }
+
var responseManager = applicationServices.GetRequiredService();
// Materialize consumers from registrations
@@ -390,6 +454,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 +524,7 @@ public MessagingRuntime Build(IServiceProvider applicationServices)
configureDelegate(descriptor);
var configuration = descriptor.CreateConfiguration();
+
var messageType = new MessageType();
messageType.Initialize(setupContext, configuration);
messageRegistry.AddMessageType(messageType);
@@ -572,22 +643,4 @@ private static void ValidateDefaultTransports(ImmutableArray
throw ThrowHelper.MultipleDefaultTransports(defaultTransportNames);
}
}
-
- private void PrepareHandlers()
- {
- foreach (var modifier in _handlerModifiers)
- {
- modifier(_handlerMiddlewares);
- }
-
- foreach (var modifier in _receiveModifiers)
- {
- modifier(_receiveMiddlewares);
- }
-
- foreach (var modifier in _dispatchModifiers)
- {
- modifier(_dispatchMiddlewares);
- }
- }
}
diff --git a/src/Mocha/src/Mocha/Builder/MessageBusBuilderExtensions.cs b/src/Mocha/src/Mocha/Builder/MessageBusBuilderExtensions.cs
index 3fe7171fa2d..882eb2ce175 100644
--- a/src/Mocha/src/Mocha/Builder/MessageBusBuilderExtensions.cs
+++ b/src/Mocha/src/Mocha/Builder/MessageBusBuilderExtensions.cs
@@ -52,12 +52,14 @@ internal static void AddDefaults(this MessageBusBuilder builder)
builder.AddMessage(x =>
{
x.AddSerializer(new JsonMessageSerializer(AcknowledgementJsonContext.Default.NotAcknowledgedEvent));
+ x.Extend().Configuration.EnclosedTypes = [typeof(NotAcknowledgedEvent)];
x.Extend().Configuration.IsInternal = true;
});
builder.AddMessage(x =>
{
x.AddSerializer(new JsonMessageSerializer(AcknowledgementJsonContext.Default.AcknowledgedEvent));
+ x.Extend().Configuration.EnclosedTypes = [typeof(AcknowledgedEvent)];
x.Extend().Configuration.IsInternal = true;
});
}
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..0732e59e21a
--- /dev/null
+++ b/src/Mocha/src/Mocha/Configuration/MessagingMessageConfiguration.cs
@@ -0,0 +1,32 @@
+using System.Collections.Immutable;
+using System.ComponentModel;
+
+namespace Mocha;
+
+///
+/// Represents a pre-built message type configuration emitted by the source generator.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+public sealed class MessagingMessageConfiguration
+{
+ ///
+ /// Gets the CLR type of the message.
+ ///
+ public required Type MessageType { get; init; }
+
+ ///
+ /// Gets the pre-built serializer for this message type.
+ ///
+ public required IMessageSerializer Serializer { get; init; }
+
+ ///
+ /// Gets the pre-computed type hierarchy sorted by specificity (most specific first). Includes both
+ /// registered user types and framework base types (,
+ /// ); branches on whether
+ /// each entry is a framework base type to decide registration versus identity-only recording.
+ /// When is ,
+ /// falls back to reflection-based hierarchy discovery
+ /// (non-AOT only).
+ ///
+ public ImmutableArray 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..2a16e4a9c6f
--- /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, or to use the runtime fallback.
+ ///
+ public ISagaStateSerializer? StateSerializer { get; init; }
+}
diff --git a/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs b/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs
index e2c3240c5c3..bff9dbb3ca4 100644
--- a/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs
+++ b/src/Mocha/src/Mocha/Descriptions/MessagingVisitor.cs
@@ -157,11 +157,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)
@@ -178,22 +174,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(IMessagingRuntime runtime, TContext context)
=> runtime is MessagingRuntime messagingRuntime ? Enter(messagingRuntime, context) : VisitorAction.Continue;
diff --git a/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs b/src/Mocha/src/Mocha/Extensions/IMessageBusHostBuilderExtensions.cs
index e282d6ba9e9..3818f7d6d69 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)
@@ -37,6 +40,8 @@ public static IMessageBusHostBuilder AddEventHandler<
/// The host builder.
/// The action to configure the consumer descriptor.
/// 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,
@@ -68,6 +73,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.
///
@@ -75,6 +127,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,
@@ -93,7 +147,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;
@@ -105,7 +162,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();
@@ -122,7 +183,10 @@ public static IMessageBusHostBuilder AddRequestHandler(this IMessageBu
/// The host builder.
/// The action to configure the consumer descriptor.
/// The builder for method chaining.
- public static IMessageBusHostBuilder AddRequestHandler(
+ [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,
Action configure)
where THandler : class, IEventRequestHandler
@@ -139,6 +203,8 @@ public static IMessageBusHostBuilder AddRequestHandler(
/// 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)
@@ -158,6 +224,8 @@ public static IMessageBusHostBuilder AddConsumer<
/// The host builder.
/// The action to configure the consumer descriptor.
/// 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..6e056f6aea8 100644
--- a/src/Mocha/src/Mocha/MessageTypes/Configurations/MessageTypeConfiguration.cs
+++ b/src/Mocha/src/Mocha/MessageTypes/Configurations/MessageTypeConfiguration.cs
@@ -1,3 +1,5 @@
+using System.Collections.Immutable;
+
namespace Mocha;
///
@@ -34,4 +36,12 @@ 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; }
+
+ ///
+ /// Gets or sets the pre-computed enclosed types from the source generator. Contains both
+ /// concrete registered types and framework base types (for example,
+ /// and ).
+ /// indicates the source generator did not supply this information.
+ ///
+ public ImmutableArray 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..bc56beaa969 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 ImmutableArray _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,48 +126,71 @@ 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",
+ "IL2026:RequiresUnreferencedCode",
+ Justification = "Reflection fallback is guarded by IsAotCompatible. AOT uses source-generated enclosed types.")]
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.IsDefaultOrEmpty)
{
- if (IsFrameworkBaseType(type))
+ foreach (var type in _enclosedTypes)
{
- // 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));
+ DispatchEnclosedType(context, type, enclosedMessageTypes, enclosedMessageIdentities);
}
- else
+ }
+ else
+ {
+ var options = context.Services.GetRequiredService();
+
+ if (options.IsAotCompatible)
{
- var mt = context.Messages.GetOrAdd(context, type);
- enclosedMessageTypes.Add(mt);
- enclosedMessageIdentities.Add(mt.Identity);
+ 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.");
}
+
+ CompleteViaReflection(context, enclosedMessageTypes, enclosedMessageIdentities);
}
EnclosedMessageTypes = enclosedMessageTypes.ToImmutableArray();
EnclosedMessageIdentities = enclosedMessageIdentities.ToImmutableArray();
- var interfaces = RuntimeType.GetInterfaces();
- foreach (var interfaceType in interfaces)
+ IsCompleted = true;
+ }
+
+ [RequiresUnreferencedCode(
+ "Walks GetInterfaces / BaseType and registers discovered types. Use the source generator for AOT.")]
+ private void CompleteViaReflection(
+ IMessagingConfigurationContext context,
+ ImmutableArray.Builder enclosedMessageTypes,
+ ImmutableArray.Builder enclosedMessageIdentities)
+ {
+ 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)
+ {
+ DispatchEnclosedType(context, type, enclosedMessageTypes, enclosedMessageIdentities);
+ }
+
+ foreach (var interfaceType in RuntimeType.GetInterfaces())
{
- if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEventRequest<>))
+ if (interfaceType.IsGenericType
+ && interfaceType.GetGenericTypeDefinition() == typeof(IEventRequest<>))
{
var responseType = interfaceType.GetGenericArguments()[0];
context.Messages.GetOrAdd(context, responseType);
}
}
-
- IsCompleted = true;
}
///
@@ -178,6 +209,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();
@@ -217,4 +249,22 @@ private static bool IsFrameworkBaseType(Type type)
return type == typeof(IEventRequest)
|| (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEventRequest<>));
}
+
+ private static void DispatchEnclosedType(
+ IMessagingConfigurationContext context,
+ Type type,
+ ImmutableArray.Builder enclosedMessageTypes,
+ ImmutableArray.Builder enclosedMessageIdentities)
+ {
+ if (IsFrameworkBaseType(type))
+ {
+ enclosedMessageIdentities.Add(context.Naming.GetMessageIdentity(type));
+ }
+ else
+ {
+ var mt = context.Messages.GetOrAdd(context, type);
+ enclosedMessageTypes.Add(mt);
+ enclosedMessageIdentities.Add(mt.Identity);
+ }
+ }
}
diff --git a/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs b/src/Mocha/src/Mocha/Middlewares/DefaultMessageBus.cs
index b0a09fde230..f09e5bb3591 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 f8e95ec0bda..0f023d4cdeb 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 56adaee0085..8281a7a8d30 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 5b6cd42c1f0..c80c6b5dc29 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 7c38237ae7a..5dc9858ddfc 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..03e7ec4e63f 100644
--- a/src/Mocha/src/Mocha/Serialization/HeadersConverter.cs
+++ b/src/Mocha/src/Mocha/Serialization/HeadersConverter.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -18,6 +19,7 @@ public class HeadersJsonConverter : JsonConverter
///
public static readonly JsonSerializerOptions Options = new() { Converters = { Instance } };
+ ///
public override Headers? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
@@ -54,6 +56,7 @@ public class HeadersJsonConverter : JsonConverter
throw new JsonException("Unexpected end of JSON");
}
+ ///
public override void Write(Utf8JsonWriter writer, IHeaders value, JsonSerializerOptions options)
{
writer.WriteStartObject();
@@ -236,9 +239,60 @@ private static void WriteValue(Utf8JsonWriter writer, object? value, JsonSeriali
writer.WriteEndObject();
break;
+ case Guid g:
+ writer.WriteStringValue(g);
+ break;
+
+ case TimeSpan t:
+ writer.WriteStringValue(t.ToString("c", CultureInfo.InvariantCulture));
+ break;
+
+ case Uri u:
+ writer.WriteStringValue(u.ToString());
+ break;
+
+ case DateOnly d:
+ writer.WriteStringValue(d.ToString("O", CultureInfo.InvariantCulture));
+ break;
+
+ case TimeOnly to:
+ writer.WriteStringValue(to.ToString("O", CultureInfo.InvariantCulture));
+ break;
+
+ case Enum e:
+ writer.WriteStringValue(e.ToString());
+ break;
+
+ case short s:
+ writer.WriteNumberValue(s);
+ break;
+
+ case ushort us:
+ writer.WriteNumberValue(us);
+ break;
+
+ case byte b:
+ writer.WriteNumberValue(b);
+ break;
+
+ case sbyte sb:
+ writer.WriteNumberValue(sb);
+ break;
+
+ case uint ui:
+ writer.WriteNumberValue(ui);
+ break;
+
+ case ulong ul:
+ writer.WriteNumberValue(ul);
+ break;
+
+ case char c:
+ writer.WriteStringValue([c]);
+ break;
+
default:
- // Fallback to default serialization for unknown types
- JsonSerializer.Serialize(writer, value, value.GetType(), options);
+ JsonSerializer.Serialize(writer, value, options.GetTypeInfo(value.GetType()));
break;
}
}
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 ed0399a28ac..69868999b79 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(TestContext.Current.CancellationToken);
}
+ [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(TestContext.Current.CancellationToken);
+ }
+
[Fact]
public async Task MO0005_CommandAndNotificationHandler_ReportsError()
{
@@ -243,4 +264,64 @@ public ValueTask HandleAsync(SomethingHappened notification, CancellationToken c
"""
]).MatchMarkdownAsync(TestContext.Current.CancellationToken);
}
+
+ [Fact]
+ public async Task MO0006_OpenGenericNotificationHandler_ReportsInfo()
+ {
+ await TestHelper.GetGeneratedSourceSnapshot(
+ [
+ """
+ using Mocha.Mediator;
+
+ namespace TestApp;
+
+ public record MyNotif : INotification;
+
+ public class GenericNotif : INotificationHandler
+ {
+ public ValueTask HandleAsync(MyNotif notification, CancellationToken cancellationToken)
+ => default;
+ }
+ """
+ ]).MatchMarkdownAsync(TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task NoWarning_ClosedConcreteHandler_NoDiagnostic()
+ {
+ await TestHelper.GetGeneratedSourceSnapshot(
+ [
+ """
+ using Mocha.Mediator;
+
+ namespace TestApp;
+
+ public record MyCommand : ICommand;
+
+ public class ConcreteHandler : ICommandHandler
+ {
+ public ValueTask HandleAsync(MyCommand command, CancellationToken cancellationToken)
+ => default;
+ }
+ """
+ ]).MatchMarkdownAsync(TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task NoWarning_UnrelatedGenericClass_NoDiagnostic()
+ {
+ await TestHelper.GetGeneratedSourceSnapshot(
+ [
+ """
+ using Mocha.Mediator;
+
+ namespace TestApp;
+
+ public class UnrelatedGeneric
+ {
+ public T? Value { get; set; }
+ }
+ """
+ ]).MatchMarkdownAsync(TestContext.Current.CancellationToken);
+ }
}
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/MessagingDiagnosticTests.cs b/src/Mocha/test/Mocha.Analyzers.Tests/MessagingDiagnosticTests.cs
index 9d550edc531..8b166adeff5 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/MessagingDiagnosticTests.cs
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/MessagingDiagnosticTests.cs
@@ -1,3 +1,5 @@
+using Microsoft.CodeAnalysis;
+
namespace Mocha.Analyzers.Tests;
public class MessagingDiagnosticTests
@@ -89,4 +91,317 @@ protected override void Configure(ISagaDescriptor descriptor) { }
"""
]).MatchMarkdownAsync(TestContext.Current.CancellationToken);
}
+
+ [Fact]
+ public void MO0015_Should_Report_When_PublishAotAndCallSiteRequiresJsonContext()
+ {
+ // arrange
+ var diagnostics = MessagingTestHelper.GetGeneratorDiagnostics(
+ [
+ """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Mocha;
+
+ namespace TestApp;
+
+ public record OrderPlacedEvent(int OrderId);
+
+ public sealed class Dispatcher
+ {
+ public ValueTask DispatchAsync(IMessageBus bus, CancellationToken cancellationToken)
+ => bus.PublishAsync(new OrderPlacedEvent(1), cancellationToken);
+ }
+ """
+ ],
+ publishAot: true);
+
+ // assert
+ var diagnostic = Assert.Single(diagnostics, d => d.Id == "MO0015");
+ Assert.Contains("Tests", diagnostic.GetMessage());
+ }
+
+ [Fact]
+ public void MO0016_Should_NotReport_When_JsonContextIsPartialAndPublishAotIsFalse()
+ {
+ // arrange
+ var diagnostics = MessagingTestHelper.GetGeneratorDiagnostics(
+ [
+ """
+ using System.Text.Json.Serialization;
+ using Mocha;
+
+ [assembly: MessagingModule("TestApp", JsonContext = typeof(TestApp.TestJsonContext))]
+
+ namespace TestApp;
+
+ [JsonSerializable(typeof(OrderPlacedEvent))]
+ public partial class TestJsonContext : JsonSerializerContext;
+
+ public record OrderPlacedEvent(int OrderId);
+
+ public record OrderAcceptedEvent(int OrderId);
+
+ public class OrderPlacedHandler : IEventHandler
+ {
+ public ValueTask HandleAsync(OrderPlacedEvent message, CancellationToken cancellationToken)
+ => default;
+ }
+
+ public class OrderAcceptedHandler : IEventHandler
+ {
+ public ValueTask HandleAsync(OrderAcceptedEvent message, CancellationToken cancellationToken)
+ => default;
+ }
+ """
+ ]);
+
+ // assert
+ Assert.DoesNotContain(diagnostics, d => d.Id == "MO0016");
+ }
+
+ [Fact]
+ public void MO0016_Should_Report_When_JsonContextIsPartialAndPublishAotIsTrue()
+ {
+ // arrange
+ var diagnostics = MessagingTestHelper.GetGeneratorDiagnostics(
+ [
+ """
+ using System.Text.Json.Serialization;
+ using Mocha;
+
+ [assembly: MessagingModule("TestApp", JsonContext = typeof(TestApp.TestJsonContext))]
+
+ namespace TestApp;
+
+ [JsonSerializable(typeof(OrderPlacedEvent))]
+ public partial class TestJsonContext : JsonSerializerContext;
+
+ public record OrderPlacedEvent(int OrderId);
+
+ public record OrderAcceptedEvent(int OrderId);
+
+ public class OrderPlacedHandler : IEventHandler
+ {
+ public ValueTask HandleAsync(OrderPlacedEvent message, CancellationToken cancellationToken)
+ => default;
+ }
+
+ public class OrderAcceptedHandler : IEventHandler
+ {
+ public ValueTask HandleAsync(OrderAcceptedEvent message, CancellationToken cancellationToken)
+ => default;
+ }
+ """
+ ],
+ publishAot: true);
+
+ // assert
+ var diagnostic = Assert.Single(diagnostics, d => d.Id == "MO0016");
+ Assert.Contains("OrderAcceptedEvent", diagnostic.GetMessage());
+ }
+
+ [Fact]
+ public void Generate_Should_NotEnableAotStrictMode_When_JsonContextSpecifiedAndPublishAotIsFalse()
+ {
+ // arrange
+ var source = CreateJsonContextSource();
+
+ // act
+ var generated = string.Join(
+ "\n",
+ MessagingTestHelper.GetGeneratedSourceTexts([source]));
+
+ // assert
+ Assert.Contains("AddJsonTypeInfoResolver", generated);
+ Assert.DoesNotContain("IsAotCompatible = true", generated);
+ }
+
+ [Fact]
+ public void Generate_Should_EnableAotStrictMode_When_JsonContextSpecifiedAndPublishAotIsTrue()
+ {
+ // arrange
+ var source = CreateJsonContextSource();
+
+ // act
+ var generated = string.Join(
+ "\n",
+ MessagingTestHelper.GetGeneratedSourceTexts([source], publishAot: true));
+
+ // assert
+ Assert.Contains("AddJsonTypeInfoResolver", generated);
+ Assert.Contains("IsAotCompatible = true", generated);
+ }
+
+ [Fact]
+ public void Generate_Should_Compile_When_SagaOnlyModuleHasNoJsonContext()
+ {
+ // arrange
+ const string source =
+ """
+ using Mocha;
+ using Mocha.Sagas;
+
+ [assembly: MessagingModule("TestApp")]
+
+ namespace TestApp;
+
+ public class OrderState : SagaStateBase;
+
+ public class OrderSaga : Saga
+ {
+ protected override void Configure(ISagaDescriptor descriptor)
+ {
+ }
+ }
+ """;
+
+ // act
+ var diagnostics = MessagingTestHelper.GetCompilationDiagnostics([source]);
+
+ // assert
+ AssertNoErrors(diagnostics);
+ }
+
+ [Fact]
+ public void Generate_Should_Compile_When_JsonContextRegistersTypeInfoResolver()
+ {
+ // arrange
+ const string source =
+ """
+ using System;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using System.Text.Json.Serialization.Metadata;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Mocha;
+
+ [assembly: MessagingModule("TestApp", JsonContext = typeof(TestApp.TestJsonContext))]
+
+ namespace TestApp;
+
+ [JsonSerializable(typeof(OrderPlacedEvent))]
+ public sealed class TestJsonContext : IJsonTypeInfoResolver
+ {
+ public static TestJsonContext Default { get; } = new();
+
+ public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+ => null;
+
+ public JsonTypeInfo? GetTypeInfo(Type type)
+ => null;
+ }
+
+ public record OrderPlacedEvent(int OrderId);
+
+ public class OrderPlacedHandler : IEventHandler
+ {
+ public ValueTask HandleAsync(OrderPlacedEvent message, CancellationToken cancellationToken)
+ => default;
+ }
+ """;
+
+ // act
+ var diagnostics = MessagingTestHelper.GetCompilationDiagnostics([source]);
+
+ // assert
+ AssertNoErrors(diagnostics);
+ }
+
+ [Fact]
+ public void MO0018_Should_Report_When_MessageBusCallSiteTypesAreMissingFromJsonContext()
+ {
+ // arrange
+ var diagnostics = MessagingTestHelper.GetGeneratorDiagnostics(
+ [
+ """
+ using System.Text.Json.Serialization;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Mocha;
+
+ [assembly: MessagingModule("TestApp", JsonContext = typeof(TestApp.TestJsonContext))]
+
+ namespace TestApp;
+
+ [JsonSerializable(typeof(RequestMessage))]
+ public sealed class TestJsonContext;
+
+ public sealed record PublishMessage;
+
+ public sealed record SendMessage;
+
+ public sealed record RequestMessage : IEventRequest;
+
+ public sealed record ResponseMessage;
+
+ public sealed class Dispatcher
+ {
+ public async ValueTask DispatchAsync(IMessageBus bus, CancellationToken cancellationToken)
+ {
+ await bus.PublishAsync(new PublishMessage(), cancellationToken);
+ await bus.SendAsync(new SendMessage(), cancellationToken);
+ await bus.RequestAsync(new RequestMessage(), cancellationToken);
+ }
+ }
+ """
+ ],
+ publishAot: true);
+
+ // assert
+ var messages = diagnostics
+ .Where(d => d.Id == "MO0018")
+ .Select(static d => d.GetMessage())
+ .OrderBy(static m => m)
+ .ToArray();
+
+ Assert.Collection(
+ messages,
+ static m =>
+ {
+ Assert.Contains("TestApp.PublishMessage", m);
+ Assert.Contains("Publish", m);
+ },
+ static m =>
+ {
+ Assert.Contains("TestApp.ResponseMessage", m);
+ Assert.Contains("Request", m);
+ },
+ static m =>
+ {
+ Assert.Contains("TestApp.SendMessage", m);
+ Assert.Contains("Send", m);
+ });
+ }
+
+ private static string CreateJsonContextSource()
+ => """
+ using System.Text.Json.Serialization;
+ using Mocha;
+
+ [assembly: MessagingModule("TestApp", JsonContext = typeof(TestApp.TestJsonContext))]
+
+ namespace TestApp;
+
+ [JsonSerializable(typeof(OrderPlacedEvent))]
+ public partial class TestJsonContext : JsonSerializerContext;
+
+ public record OrderPlacedEvent(int OrderId);
+
+ public class OrderPlacedHandler : IEventHandler
+ {
+ public ValueTask HandleAsync(OrderPlacedEvent message, CancellationToken cancellationToken)
+ => default;
+ }
+ """;
+
+ private static void AssertNoErrors(IReadOnlyCollection diagnostics)
+ {
+ var errors = diagnostics
+ .Where(static d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
+ .ToArray();
+
+ Assert.True(errors.Length == 0, string.Join(Environment.NewLine, errors.Select(static d => d.ToString())));
+ }
}
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/MessagingTestHelper.cs b/src/Mocha/test/Mocha.Analyzers.Tests/MessagingTestHelper.cs
index abd53262e86..c4802d47f97 100644
--- a/src/Mocha/test/Mocha.Analyzers.Tests/MessagingTestHelper.cs
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/MessagingTestHelper.cs
@@ -3,10 +3,12 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
+using System.Threading.Tasks;
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using CookieCrumble;
+using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
namespace Mocha.Analyzers.Tests;
@@ -18,7 +20,87 @@ internal static class MessagingTestHelper
public static Snapshot GetGeneratedSourceSnapshot(
string[] sourceTexts,
- string? assemblyName = "Tests")
+ string? assemblyName = "Tests",
+ bool publishAot = false)
+ {
+ var driver = RunGenerator(sourceTexts, assemblyName, publishAot);
+ var snapshot = new Snapshot();
+
+ foreach (var result in driver.GetRunResult().Results)
+ {
+ var sources = result.GeneratedSources.OrderBy(s => s.HintName);
+ foreach (var source in sources)
+ {
+ snapshot.Add(source.SourceText.ToString(), source.HintName, MarkdownLanguages.CSharp);
+ }
+
+ if (result.Diagnostics.Any())
+ {
+ AddDiagnosticsToSnapshot(snapshot, result.Diagnostics, "Generator Diagnostics");
+ }
+ }
+
+ return snapshot;
+ }
+
+ public static ImmutableArray GetGeneratorDiagnostics(
+ string[] sourceTexts,
+ string? assemblyName = "Tests",
+ bool publishAot = false)
+ {
+ var driver = RunGenerator(sourceTexts, assemblyName, publishAot);
+
+ return driver.GetRunResult().Results
+ .SelectMany(static r => r.Diagnostics)
+ .ToImmutableArray();
+ }
+
+ public static ImmutableArray GetCompilationDiagnostics(
+ string[] sourceTexts,
+ string? assemblyName = "Tests",
+ bool publishAot = false)
+ {
+ var compilation = CreateCompilation(sourceTexts, assemblyName);
+ var driver = CreateDriver(publishAot);
+
+ driver = driver.RunGeneratorsAndUpdateCompilation(
+ compilation,
+ out var outputCompilation,
+ out var generatorDiagnostics);
+
+ return generatorDiagnostics
+ .Concat(outputCompilation.GetDiagnostics())
+ .Where(static d => !s_ignoreCodes.Contains(d.Id))
+ .ToImmutableArray();
+ }
+
+ public static ImmutableArray GetGeneratedSourceTexts(
+ string[] sourceTexts,
+ string? assemblyName = "Tests",
+ bool publishAot = false)
+ {
+ var driver = RunGenerator(sourceTexts, assemblyName, publishAot);
+
+ return driver.GetRunResult().Results
+ .SelectMany(static r => r.GeneratedSources)
+ .OrderBy(static s => s.HintName)
+ .Select(static s => s.SourceText.ToString())
+ .ToImmutableArray();
+ }
+
+ private static GeneratorDriver RunGenerator(
+ string[] sourceTexts,
+ string? assemblyName,
+ bool publishAot)
+ {
+ var compilation = CreateCompilation(sourceTexts, assemblyName);
+
+ return CreateDriver(publishAot).RunGenerators(compilation);
+ }
+
+ private static CSharpCompilation CreateCompilation(
+ string[] sourceTexts,
+ string? assemblyName)
{
IEnumerable references =
[
@@ -35,12 +117,23 @@ public static Snapshot GetGeneratedSourceSnapshot(
// Mocha (IConsumer, IBatchEventHandler, Saga, SagaStateBase)
MetadataReference.CreateFromFile(typeof(IConsumer).Assembly.Location),
+#if NET11_0
+ // System.Collections.Immutable
+ MetadataReference.CreateFromFile(typeof(ImmutableArray<>).Assembly.Location),
+#endif
+
+ // System.Text.Json
+ MetadataReference.CreateFromFile(typeof(JsonSerializerOptions).Assembly.Location),
+
// Microsoft.Extensions.DependencyInjection.Abstractions
MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location),
// System.Runtime.CompilerServices.Unsafe
MetadataReference.CreateFromFile(typeof(Unsafe).Assembly.Location),
+ // System.Threading.Tasks.Extensions
+ MetadataReference.CreateFromFile(typeof(ValueTask).Assembly.Location),
+
// System.Runtime from the actual runtime (needed for predefined type resolution
// so that assembly-level attribute constructor arguments can be bound)
MetadataReference.CreateFromFile(
@@ -49,33 +142,18 @@ public static Snapshot GetGeneratedSourceSnapshot(
"System.Runtime.dll"))
];
- var compilation = CSharpCompilation.Create(
+ return CSharpCompilation.Create(
assemblyName: assemblyName,
syntaxTrees: sourceTexts.Select(s => CSharpSyntaxTree.ParseText(s)),
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+ private static GeneratorDriver CreateDriver(bool publishAot)
+ {
var generator = new MessagingGenerator();
- GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
- driver = driver.RunGenerators(compilation);
-
- var snapshot = new Snapshot();
-
- foreach (var result in driver.GetRunResult().Results)
- {
- var sources = result.GeneratedSources.OrderBy(s => s.HintName);
- foreach (var source in sources)
- {
- snapshot.Add(source.SourceText.ToString(), source.HintName, MarkdownLanguages.CSharp);
- }
-
- if (result.Diagnostics.Any())
- {
- AddDiagnosticsToSnapshot(snapshot, result.Diagnostics, "Generator Diagnostics");
- }
- }
-
- return snapshot;
+ return CSharpGeneratorDriver.Create(generator)
+ .WithUpdatedAnalyzerConfigOptions(new TestAnalyzerConfigOptionsProvider(publishAot));
}
private static void AddDiagnosticsToSnapshot(
@@ -161,4 +239,42 @@ private static void AddDiagnosticsToSnapshot(
snapshot.Add(Encoding.UTF8.GetString(stream.ToArray()), title, MarkdownLanguages.Json);
}
}
+
+ private sealed class TestAnalyzerConfigOptionsProvider(bool publishAot)
+ : AnalyzerConfigOptionsProvider
+ {
+ private readonly AnalyzerConfigOptions _globalOptions =
+ new TestAnalyzerConfigOptions(
+ new Dictionary
+ {
+ ["build_property.PublishAot"] = publishAot ? "true" : "false"
+ });
+
+ public override AnalyzerConfigOptions GlobalOptions => _globalOptions;
+
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
+ => TestAnalyzerConfigOptions.Empty;
+
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
+ => TestAnalyzerConfigOptions.Empty;
+ }
+
+ private sealed class TestAnalyzerConfigOptions(IReadOnlyDictionary options)
+ : AnalyzerConfigOptions
+ {
+ public static readonly AnalyzerConfigOptions Empty =
+ new TestAnalyzerConfigOptions(new Dictionary());
+
+ public override bool TryGetValue(string key, out string value)
+ {
+ if (options.TryGetValue(key, out var result))
+ {
+ value = result;
+ return true;
+ }
+
+ value = "";
+ return false;
+ }
+ }
}
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..7249f0a5ffd 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,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.CreateOrderCommand),
+ },
+ 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..48a2eb72939 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,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(global::TestApp.DeleteOrderCommand),
+ },
+ 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..ea9e24ee371 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,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),
+ },
+ 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..f61680ccd8d 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}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "Message": "Handler 'global::TestApp.GenericCommandHandler' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "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..e842d4e8f68 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}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "Message": "Handler 'global::TestApp.GenericQueryHandler' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "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..ff71071dce8
--- /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}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "Message": "Handler 'global::TestApp.GenericHandler' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "Category": "Mediator",
+ "CustomTags": []
+ }
+]
+```
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0006_OpenGenericNotificationHandler_ReportsInfo.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0006_OpenGenericNotificationHandler_ReportsInfo.md
new file mode 100644
index 00000000000..df75e1f23b6
--- /dev/null
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.MO0006_OpenGenericNotificationHandler_ReportsInfo.md
@@ -0,0 +1,17 @@
+# MO0006_OpenGenericNotificationHandler_ReportsInfo
+
+```json
+[
+ {
+ "Id": "MO0006",
+ "Title": "Open generic handler cannot be auto-registered",
+ "Severity": "Info",
+ "WarningLevel": 1,
+ "Location": ": (6,13)-(6,25)",
+ "MessageFormat": "Handler '{0}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "Message": "Handler 'global::TestApp.GenericNotif' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.",
+ "Category": "Mediator",
+ "CustomTags": []
+ }
+]
+```
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_ClosedConcreteHandler_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_ClosedConcreteHandler_NoDiagnostic.md
new file mode 100644
index 00000000000..61b31116543
--- /dev/null
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_ClosedConcreteHandler_NoDiagnostic.md
@@ -0,0 +1,43 @@
+# NoWarning_ClosedConcreteHandler_NoDiagnostic
+
+```csharp
+//
+
+#nullable enable
+#pragma warning disable
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Mocha.Analyzers", "1.0.0")]
+ public static class TestsMediatorBuilderExtensions
+ {
+ [global::Mocha.Mediator.MediatorModuleInfo(
+ MessageTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.MyCommand),
+ },
+ HandlerTypes = new global::System.Type[]
+ {
+ typeof(global::TestApp.ConcreteHandler),
+ }
+ )]
+ 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.ConcreteHandler),
+ MessageType = typeof(global::TestApp.MyCommand),
+ Kind = global::Mocha.Mediator.MediatorHandlerKind.Command,
+ Delegate = global::Mocha.Mediator.PipelineBuilder.BuildCommandPipeline()
+ });
+
+ return builder;
+ }
+ }
+}
+
+```
diff --git a/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.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__/DiagnosticTests.NoWarning_UnrelatedGenericClass_NoDiagnostic.md b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_UnrelatedGenericClass_NoDiagnostic.md
new file mode 100644
index 00000000000..2474de325ca
--- /dev/null
+++ b/src/Mocha/test/Mocha.Analyzers.Tests/__snapshots__/DiagnosticTests.NoWarning_UnrelatedGenericClass_NoDiagnostic.md
@@ -0,0 +1,2 @@
+# NoWarning_UnrelatedGenericClass_NoDiagnostic
+
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..fc16e3b6cc6 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,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.ProcessCommand),
+ },
+ 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..d4ba158f179 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,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.ComputeCommand),
+ },
+ 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..29ee2fe5ea7 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,22 @@ 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),
+ },
+ 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..56c885287d7 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,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.GetItemQuery),
+ },
+ 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..96b670f95dc 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,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.CreateInvoiceCommand),
+ },
+ 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..cbf38d5a9dd 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,23 @@ 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),
+ },
+ 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..c6cff9d1c14 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,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.Orders.CreateOrderCommand),
+ typeof(global::TestApp.Users.GetUserQuery),
+ },
+ 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..e0e9b6d649a 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,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.CreateOrderCommand),
+ },
+ 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..fd9456b352f 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,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.GetOrderQuery),
+ },
+ 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..8bd87bae89d 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,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.GetOrderQuery),
+ typeof(global::TestApp.GetUserQuery),
+ },
+ 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..b2f784147f4 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,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.GetUserQuery),
+ },
+ 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..47dfc462278 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,22 @@ 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),
+ },
+ 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/MessageTypes/AotMessageTypeTests.cs b/src/Mocha/test/Mocha.Tests/MessageTypes/AotMessageTypeTests.cs
new file mode 100644
index 00000000000..7537e5aa9c4
--- /dev/null
+++ b/src/Mocha/test/Mocha.Tests/MessageTypes/AotMessageTypeTests.cs
@@ -0,0 +1,203 @@
+using System.Buffers;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.DependencyInjection;
+using Mocha.Events;
+using Mocha.Transport.InMemory;
+
+namespace Mocha.Tests;
+
+public class AotMessageTypeTests
+{
+ [Fact]
+ public void Complete_Should_ResolveFrameworkIdentityViaRuntimeNaming_When_AotConfigurationIncludesFrameworkBaseType()
+ {
+ // arrange
+ var runtime = CreateAotRuntime(builder => builder.ConfigureMessageBus(bus =>
+ {
+ bus.AddMessageConfiguration(new MessagingMessageConfiguration
+ {
+ MessageType = typeof(GetOrderStatus),
+ Serializer = new StubMessageSerializer(),
+ EnclosedTypes = [typeof(GetOrderStatus), typeof(IEventRequest)]
+ });
+
+ bus.AddMessageConfiguration(new MessagingMessageConfiguration
+ {
+ MessageType = typeof(OrderStatusResponse),
+ Serializer = new StubMessageSerializer(),
+ EnclosedTypes = [typeof(OrderStatusResponse)]
+ });
+ }));
+
+ // act
+ var messageType = runtime.Messages.GetMessageType(typeof(GetOrderStatus))!;
+ var expected = runtime.Naming.GetMessageIdentity(typeof(IEventRequest));
+
+ // assert
+ Assert.True(messageType.IsCompleted);
+ Assert.Contains(expected, messageType.EnclosedMessageIdentities);
+ }
+
+ [Fact]
+ public void Complete_Should_UseCustomNamingConvention_When_FrameworkBaseTypeResolvedViaRuntimeNaming()
+ {
+ // arrange
+ var runtime = CreateAotRuntime(
+ builder =>
+ {
+ // Registered on the outer service provider because MessageBusBuilder.AddCoreServices
+ // resolves IBusNamingConventions from application services.
+ builder.Services.AddSingleton();
+
+ builder.ConfigureMessageBus(bus =>
+ {
+ bus.AddMessageConfiguration(new MessagingMessageConfiguration
+ {
+ MessageType = typeof(GetOrderStatus),
+ Serializer = new StubMessageSerializer(),
+ EnclosedTypes = [typeof(GetOrderStatus), typeof(IEventRequest)]
+ });
+
+ bus.AddMessageConfiguration(new MessagingMessageConfiguration
+ {
+ MessageType = typeof(OrderStatusResponse),
+ Serializer = new StubMessageSerializer(),
+ EnclosedTypes = [typeof(OrderStatusResponse)]
+ });
+ });
+ });
+
+ // act
+ var messageType = runtime.Messages.GetMessageType(typeof(GetOrderStatus))!;
+ var defaultIdentity = new DefaultNamingConventions(runtime.Host)
+ .GetMessageIdentity(typeof(IEventRequest));
+
+ // assert
+ Assert.True(messageType.IsCompleted);
+ Assert.Contains(
+ $"custom:{typeof(IEventRequest).FullName}",
+ messageType.EnclosedMessageIdentities);
+ Assert.DoesNotContain(defaultIdentity, messageType.EnclosedMessageIdentities);
+ }
+
+ [Fact]
+ public void Complete_Should_Throw_When_AotModeWithoutEnclosedTypes()
+ {
+ // act & assert
+ var exception = Assert.Throws(Act);
+ Assert.Contains("No enclosed types provided", exception.Message);
+
+ static void Act()
+ {
+ CreateAotRuntime(
+ builder => builder.ConfigureMessageBus(bus => bus.AddMessage(static _ => { })));
+ }
+ }
+
+ [Fact]
+ public void Complete_Should_RegisterInternalAcknowledgementEvents_When_AotMode()
+ {
+ // arrange
+ var runtime = CreateAotRuntime(static _ => { });
+
+ // act
+ var acknowledged = runtime.Messages.GetMessageType(typeof(AcknowledgedEvent))!;
+ var notAcknowledged = runtime.Messages.GetMessageType(typeof(NotAcknowledgedEvent))!;
+
+ // assert
+ Assert.True(acknowledged.IsCompleted);
+ Assert.Contains(acknowledged.Identity, acknowledged.EnclosedMessageIdentities);
+ Assert.True(notAcknowledged.IsCompleted);
+ Assert.Contains(notAcknowledged.Identity, notAcknowledged.EnclosedMessageIdentities);
+ }
+
+ [Fact]
+ public void GetSerializer_Should_UseConfiguredJsonTypeInfoResolver_When_AotMode()
+ {
+ // arrange
+ var runtime = CreateAotRuntime(builder =>
+ {
+ builder.AddJsonTypeInfoResolver(AotMessageTypeJsonContext.Default);
+ builder.ConfigureMessageBus(bus => bus.AddMessage(
+ static descriptor => descriptor.Extend().Configuration.EnclosedTypes = [typeof(GetOrderStatus)]));
+ });
+
+ var messageType = runtime.Messages.GetMessageType(typeof(GetOrderStatus))!;
+
+ // act
+ var serializer = messageType.GetSerializer(MessageContentType.Json);
+ var writer = new ArrayBufferWriter();
+ serializer!.Serialize(new GetOrderStatus { OrderId = "42" }, writer);
+ var deserialized = serializer.Deserialize(writer.WrittenMemory);
+
+ // assert
+ Assert.Equal("42", deserialized?.OrderId);
+ }
+
+ public sealed class GetOrderStatus : IEventRequest
+ {
+ public string OrderId { get; init; } = "";
+ }
+
+ public sealed class OrderStatusResponse
+ {
+ public string Status { get; init; } = "";
+ }
+
+ private sealed class StubMessageSerializer : IMessageSerializer
+ {
+ public MessageContentType ContentType => MessageContentType.Json;
+
+ public T? Deserialize(ReadOnlyMemory body) => default;
+
+ public object? Deserialize(ReadOnlyMemory body) => null;
+
+ public void Serialize(T message, IBufferWriter writer)
+ {
+ }
+
+ public void Serialize(object message, IBufferWriter writer)
+ {
+ }
+ }
+
+ ///
+ /// Test double that prefixes every identity with "custom:" so we can observe whether
+ /// the runtime resolves framework base type identities through the configured convention
+ /// instead of a compile-time-baked URN.
+ ///
+ private sealed class PrefixingNamingConventions : IBusNamingConventions
+ {
+ public string GetReceiveEndpointName(InboundRoute route, ReceiveEndpointKind kind) => route.Consumer?.Name ?? "unknown";
+
+ public string GetReceiveEndpointName(Type handlerType, ReceiveEndpointKind kind) => handlerType.Name;
+
+ public string GetReceiveEndpointName(string name, ReceiveEndpointKind kind) => name;
+
+ public string GetSagaName(Type sagaType) => sagaType.Name;
+
+ public string GetInstanceEndpoint(Guid instanceId) => $"response-{instanceId:N}";
+
+ public string GetSendEndpointName(Type messageType) => messageType.Name;
+
+ public string GetPublishEndpointName(Type messageType) => messageType.Name;
+
+ public string GetMessageIdentity(Type messageType) => $"custom:{messageType.FullName}";
+ }
+
+ private static MessagingRuntime CreateAotRuntime(Action configure)
+ {
+ var services = new ServiceCollection();
+ var builder = services.AddMessageBus();
+ builder.ModifyOptions(static o => o.IsAotCompatible = true);
+
+ configure(builder);
+ builder.AddInMemory();
+
+ var provider = services.BuildServiceProvider();
+ return (MessagingRuntime)provider.GetRequiredService();
+ }
+}
+
+[JsonSerializable(typeof(AotMessageTypeTests.GetOrderStatus))]
+internal partial class AotMessageTypeJsonContext : JsonSerializerContext;
diff --git a/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs b/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs
index 69f75e1b528..bcc2a745587 100644
--- a/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs
+++ b/src/Mocha/test/Mocha.Tests/Middlewares/Receive/ReceiveDeadLetterMiddlewareTests.cs
@@ -634,6 +634,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/src/Mocha/test/Mocha.Tests/Serialization/HeadersJsonConverterTests.cs b/src/Mocha/test/Mocha.Tests/Serialization/HeadersJsonConverterTests.cs
new file mode 100644
index 00000000000..233950a516f
--- /dev/null
+++ b/src/Mocha/test/Mocha.Tests/Serialization/HeadersJsonConverterTests.cs
@@ -0,0 +1,223 @@
+using System.Globalization;
+using System.Text.Json;
+
+namespace Mocha.Tests;
+
+public class HeadersJsonConverterTests
+{
+ [Fact]
+ public void Write_Should_EmitStringValue_When_HeaderIsGuid()
+ {
+ // arrange
+ var headers = new Headers();
+ var value = Guid.Parse("12345678-1234-1234-1234-123456789012");
+ headers.Set("id", value);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"id":"12345678-1234-1234-1234-123456789012"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitIsoString_When_HeaderIsTimeSpan()
+ {
+ // arrange
+ var headers = new Headers();
+ var value = new TimeSpan(1, 2, 3, 4);
+ headers.Set("duration", value);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ var expected = value.ToString("c", CultureInfo.InvariantCulture);
+ Assert.Equal($$"""{"duration":"{{expected}}"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitStringValue_When_HeaderIsUri()
+ {
+ // arrange
+ var headers = new Headers();
+ var value = new Uri("https://example.com/path");
+ headers.Set("url", value);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"url":"https://example.com/path"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitIsoDate_When_HeaderIsDateOnly()
+ {
+ // arrange
+ var headers = new Headers();
+ var value = new DateOnly(2024, 1, 15);
+ headers.Set("date", value);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ var expected = value.ToString("O", CultureInfo.InvariantCulture);
+ Assert.Equal($$"""{"date":"{{expected}}"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitIsoTime_When_HeaderIsTimeOnly()
+ {
+ // arrange
+ var headers = new Headers();
+ var value = new TimeOnly(10, 30, 45);
+ headers.Set("time", value);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ var expected = value.ToString("O", CultureInfo.InvariantCulture);
+ Assert.Equal($$"""{"time":"{{expected}}"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitEnumName_When_HeaderIsEnum()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("priority", TestPriority.High);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"priority":"High"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitNumber_When_HeaderIsShort()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("value", (short)123);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"value":123}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitNumber_When_HeaderIsUshort()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("value", (ushort)456);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"value":456}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitNumber_When_HeaderIsByte()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("value", (byte)7);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"value":7}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitNumber_When_HeaderIsSbyte()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("value", (sbyte)-8);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"value":-8}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitNumber_When_HeaderIsUint()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("value", 4000000000u);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"value":4000000000}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitNumber_When_HeaderIsUlong()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("value", 18000000000000000000ul);
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"value":18000000000000000000}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_EmitStringValue_When_HeaderIsChar()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("letter", 'A');
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"letter":"A"}""", json);
+ }
+
+ [Fact]
+ public void Write_Should_SerializeCustomType_When_HeaderIsCustomType()
+ {
+ // arrange
+ var headers = new Headers();
+ headers.Set("custom", new CustomHeaderDto { Name = "test" });
+
+ // act
+ var json = JsonSerializer.Serialize(headers, HeadersJsonConverter.Options);
+
+ // assert
+ Assert.Equal("""{"custom":{"Name":"test"}}""", json);
+ }
+
+ private enum TestPriority
+ {
+ Low,
+ Normal,
+ High
+ }
+
+ private sealed class CustomHeaderDto
+ {
+ public string Name { get; init; } = "";
+ }
+}
diff --git a/website-next/content/docs/mocha/diagnostics.md b/website-next/content/docs/mocha/diagnostics.md
index 747a0d6042b..a489ebd3476 100644
--- a/website-next/content/docs/mocha/diagnostics.md
+++ b/website-next/content/docs/mocha/diagnostics.md
@@ -14,10 +14,15 @@ Mocha uses a Roslyn source generator to validate your message handlers, consumer
| [MO0003](#mo0003) | Handler is abstract | Warning |
| [MO0004](#mo0004) | Open generic message type cannot be dispatched | Info |
| [MO0005](#mo0005) | Handler implements multiple mediator handler interfaces | Error |
+| [MO0006](#mo0006) | Open generic handler cannot be auto-registered | Info |
| [MO0011](#mo0011) | Duplicate handler for request type | Error |
| [MO0012](#mo0012) | Open generic messaging handler cannot be auto-registered | Info |
| [MO0013](#mo0013) | Messaging handler is abstract | Warning |
| [MO0014](#mo0014) | Saga must have a public parameterless constructor | Error |
+| [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
@@ -255,6 +260,79 @@ public class GetOrderHandler : IQueryHandler
}
```
+## MO0006
+
+**Open generic handler cannot be auto-registered**
+
+| | |
+| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Severity** | Info |
+| **Message** | `Handler '{0}' has unbound type parameters; source-generated registration is skipped. Register the closed form manually (e.g. AddHandler>()) if intentional.` |
+
+### Cause
+
+A mediator handler (`ICommandHandler`, `ICommandHandler`, `IQueryHandler`, or `INotificationHandler`) is declared with unbound type parameters. This diagnostic fires on the handler _declaration_ - the source generator cannot produce a registration for an open generic because there is no closed type to register.
+
+Under AOT, this matters because every dispatched message must correspond to a statically-known handler type; the runtime cannot close the generic for you.
+
+### Example
+
+```csharp
+using Mocha.Mediator;
+
+public record MyNotification : INotification;
+
+// Open generic handler - triggers MO0006
+public class GenericNotificationHandler : INotificationHandler
+{
+ public ValueTask HandleAsync(
+ MyNotification notification,
+ CancellationToken ct)
+ => ValueTask.CompletedTask;
+}
+```
+
+### Fix
+
+Either close the generic by declaring a concrete handler, or register the closed form manually and suppress the diagnostic on the declaration.
+
+Close the generic at the handler declaration:
+
+```csharp
+using Mocha.Mediator;
+
+public record MyNotification : INotification;
+
+public class MyNotificationHandler : INotificationHandler
+{
+ public ValueTask HandleAsync(
+ MyNotification notification,
+ CancellationToken ct)
+ => ValueTask.CompletedTask;
+}
+```
+
+Or, if the open generic is intentional (for example, a reusable handler body parameterized on a secondary type), register the closed form manually and suppress MO0006 on the declaration:
+
+```csharp
+using Mocha.Mediator;
+
+public record MyNotification : INotification;
+
+#pragma warning disable MO0006
+public class GenericNotificationHandler : INotificationHandler
+{
+ public ValueTask HandleAsync(
+ MyNotification notification,
+ CancellationToken ct)
+ => ValueTask.CompletedTask;
+}
+#pragma warning restore MO0006
+
+// Register the closed form explicitly:
+// services.AddMediator(m => m.AddHandler>());
+```
+
# Messaging diagnostics
These diagnostics apply to the [message bus](./handlers-and-consumers.md) - event handlers, request handlers, batch handlers, consumers, and sagas that communicate across service boundaries.
@@ -464,3 +542,213 @@ 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
+
+Emitted only when `PublishAot` is enabled and a call site publishes a type not covered by the declared `JsonContext`. For example, `bus.PublishAsync()` is called with a type that has no matching `[JsonSerializable(typeof(T))]` on the `JsonSerializerContext` referenced by `[assembly: MessagingModule(..., JsonContext = typeof(...))]`. 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.
+
+This diagnostic does not fire when `PublishAot` is unset, even if `JsonContext` is declared on the module - opting into a source-generated context for non-AOT reasons does not require every call-site type to appear in it.
+
+### 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;
+ }
+}
+```
diff --git a/website/src/docs/mocha/v16/sagas.md b/website/src/docs/mocha/v16/sagas.md
index 938355d5453..9de4ffa1f3c 100644
--- a/website/src/docs/mocha/v16/sagas.md
+++ b/website/src/docs/mocha/v16/sagas.md
@@ -619,11 +619,11 @@ The correlation lookup order is:
# Timeouts
-A saga that waits for a message that never arrives will stay in its current state forever. Timeouts ensure every saga eventually completes — either through normal processing or by timing out.
+A saga that waits for a message that never arrives will stay in its current state forever. Timeouts ensure every saga eventually completes - either through normal processing or by timing out.
Mocha provides a saga-level `Timeout()` API that sets a single deadline for the entire saga instance. The timeout is scheduled when the saga is created and automatically cancelled when the saga reaches any final state.
-> **Prerequisites:** Durable, cancellable timeouts require a scheduling store. Configure `UsePostgresScheduling()` before using `Timeout()` — see [Scheduling: Set up store-based scheduling](/docs/mocha/v16/scheduling#set-up-store-based-scheduling-for-rabbitmq) for setup instructions. Native transport scheduling (InMemory, PostgreSQL) also works but does not support automatic cancellation.
+> **Prerequisites:** Durable, cancellable timeouts require a scheduling store. Configure `UsePostgresScheduling()` before using `Timeout()` - see [Scheduling: Set up store-based scheduling](/docs/mocha/v16/scheduling#set-up-store-based-scheduling-for-rabbitmq) for setup instructions. Native transport scheduling (InMemory, PostgreSQL) also works but does not support automatic cancellation.
## Configure a saga-level timeout
@@ -632,7 +632,7 @@ Call `Timeout()` on the saga descriptor to set a deadline that applies to the en
```csharp
protected override void Configure(ISagaDescriptor descriptor)
{
- // 30-minute timeout — saga will time out if it doesn't reach a final state
+ // 30-minute timeout - saga will time out if it doesn't reach a final state
descriptor.Timeout(TimeSpan.FromMinutes(30))
.Respond(state => new OrderTimedOutResponse(state.Id));
@@ -660,9 +660,9 @@ protected override void Configure(ISagaDescriptor descriptor)
2. Returns an `ISagaFinalStateDescriptor` so you can chain `.Respond()` to send a response when the saga times out.
3. Tells the saga framework to schedule a timeout event when a new saga instance is created.
-The timeout clock starts when the saga is created — that is, when the first event arrives and a new instance is provisioned. If the saga reaches any final state before the deadline, the pending timeout is automatically cancelled. If the timeout fires, a `SagaTimedOutEvent` is delivered to the saga instance.
+The timeout clock starts when the saga is created - that is, when the first event arrives and a new instance is provisioned. If the saga reaches any final state before the deadline, the pending timeout is automatically cancelled. If the timeout fires, a `SagaTimedOutEvent` is delivered to the saga instance.
-Use `OnTimeout()` on a state descriptor to define what happens when the timeout arrives. Handle it the same way you handle any other event — run `.Then()` actions, dispatch commands, or transition to a different state.
+Use `OnTimeout()` on a state descriptor to define what happens when the timeout arrives. Handle it the same way you handle any other event - run `.Then()` actions, dispatch commands, or transition to a different state.
## Key behaviors
@@ -675,7 +675,7 @@ Use `OnTimeout()` on a state descriptor to define what happens when the timeout
| Missing handler | If no `OnTimeout()` handler is configured for the current state, the saga throws an execution error. See [troubleshooting](#timeout-troubleshooting) below. |
| Recommended pattern | `DuringAny().OnTimeout()` handles the timeout regardless of which state the saga is in. |
| Response on timeout | Chain `.Respond()` on the timed-out final state to send a response back to the original requester. |
-| Scheduling store | Requires a scheduling store for durable timeouts. Configure `UsePostgresScheduling()` — see [Scheduling](/docs/mocha/v16/scheduling#set-up-store-based-scheduling-for-rabbitmq) for setup. Native transport scheduling (InMemory, PostgreSQL) also works but does not support cancellation. |
+| Scheduling store | Requires a scheduling store for durable timeouts. Configure `UsePostgresScheduling()` - see [Scheduling](/docs/mocha/v16/scheduling#set-up-store-based-scheduling-for-rabbitmq) for setup. Native transport scheduling (InMemory, PostgreSQL) also works but does not support cancellation. |
## Timeout troubleshooting
@@ -697,9 +697,9 @@ descriptor.DuringAny()
| Method | Available on | Parameters | Description |
| ----------- | ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `Initially` | `ISagaDescriptor