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