--- url: /guide/cascading-messages.md --- # Cascading Messages Cascading messages is a powerful feature in Foundatio Mediator that allows handlers to automatically publish additional messages when they complete. This enables clean event-driven architectures and decoupled business logic. ## How Cascading Works When a handler returns a tuple, the mediator performs type matching to determine which value to return to the caller and which values to publish as cascading messages: 1. **Type Matching**: The mediator finds the tuple element that matches the requested return type from `Invoke` 2. **Return to Caller**: That matched element is returned to the caller 3. **Cascading Publishing**: All remaining non-null tuple elements are automatically published as messages ```csharp public class OrderHandler { public static (Result, OrderCreated, EmailNotification) Handle(CreateOrderCommand command) { var order = new Order { Id = Guid.NewGuid(), Email = command.Email }; // Return tuple with multiple values return ( Result.Created(order, $"/orders/{order.Id}"), // Matches Invoke> new OrderCreated(order.Id, order.Email), // Auto-published new EmailNotification(order.Email, "Order Created") // Auto-published ); } } // Usage - Result is returned, events are published var result = await mediator.InvokeAsync>(new CreateOrderCommand("test@example.com")); // result contains the Result from the tuple // OrderCreated and EmailNotification are automatically published ``` ### Type Matching Examples **Exact Type Match:** ```csharp // Handler returns (string, OrderCreated) public static (string, OrderCreated) Handle(GetOrderStatusQuery query) { return ("Processing", new OrderCreated(query.OrderId, "user@example.com")); } // Call with string return type var status = await mediator.InvokeAsync(new GetOrderStatusQuery("123")); // status = "Processing", OrderCreated is published ``` **Interface/Base Class Matching:** ```csharp // Handler returns (Result, OrderCreated) public static (Result, OrderCreated) Handle(CreateOrderCommand command) { var order = new Order(); return (Result.Created(order), new OrderCreated(order.Id, order.Email)); } // Can call with base Result type var result = await mediator.InvokeAsync(new CreateOrderCommand("test@example.com")); // Result matches Result, OrderCreated is published ``` ## Tuple Return Patterns ### Basic Event Publishing ```csharp public static (Order, OrderCreated) Handle(CreateOrderCommand command) { var order = new Order { Email = command.Email }; var orderCreated = new OrderCreated(order.Id, order.Email); return (order, orderCreated); // OrderCreated will be published automatically } ``` ### Multiple Events ```csharp public static (Order, OrderCreated, CustomerUpdated, InventoryReserved) Handle(CreateOrderCommand command) { var order = new Order { Email = command.Email, ProductId = command.ProductId }; return ( order, // Response new OrderCreated(order.Id, order.Email), // Event 1 new CustomerUpdated(command.CustomerId), // Event 2 new InventoryReserved(command.ProductId, 1) // Event 3 ); } ``` ### Conditional Event Publishing ```csharp public static (Result, OrderCreated?, CustomerWelcomeEmail?) Handle(CreateOrderCommand command) { var order = new Order { Email = command.Email }; // Only publish welcome email for new customers var isNewCustomer = CheckIfNewCustomer(command.Email); return ( order, new OrderCreated(order.Id, order.Email), // Always published isNewCustomer ? new CustomerWelcomeEmail(command.Email) : null // Conditional ); } ``` ## Real-World Example Here's a complete e-commerce order processing example: ### Messages ```csharp // Commands public record CreateOrderCommand(string Email, string ProductId, int Quantity); // Events public record OrderCreated(string OrderId, string Email, DateTime CreatedAt); public record InventoryReserved(string ProductId, int Quantity); public record PaymentProcessed(string OrderId, decimal Amount); public record CustomerNotified(string Email, string Subject, string Message); ``` ### Order Handler with Cascading ```csharp public class OrderHandler { public static (Result, OrderCreated, InventoryReserved, CustomerNotified) Handle( CreateOrderCommand command, IOrderRepository repository, ILogger logger) { logger.LogInformation("Creating order for {Email}", command.Email); var order = new Order { Id = Guid.NewGuid().ToString(), Email = command.Email, ProductId = command.ProductId, Quantity = command.Quantity, Status = OrderStatus.Created, CreatedAt = DateTime.UtcNow }; repository.Save(order); // Return response + cascade events return ( Result.Created(order, $"/orders/{order.Id}"), new OrderCreated(order.Id, order.Email, order.CreatedAt), new InventoryReserved(order.ProductId, order.Quantity), new CustomerNotified(order.Email, "Order Confirmation", $"Order {order.Id} created") ); } } ``` ### Event Handlers Each cascaded event can have its own handlers: ```csharp // Handle inventory reservation public class InventoryHandler { public static async Task Handle(InventoryReserved @event, IInventoryService inventory) { await inventory.ReserveAsync(@event.ProductId, @event.Quantity); } } // Handle customer notifications public class NotificationHandler { public static async Task Handle(CustomerNotified @event, IEmailService email) { await email.SendAsync(@event.Email, @event.Subject, @event.Message); } } // Handle order analytics public class AnalyticsHandler { public static async Task Handle(OrderCreated @event, IAnalyticsService analytics) { await analytics.TrackOrderCreatedAsync(@event.OrderId, @event.Email); } } ``` ## Complex Cascading Scenarios ### Workflow Orchestration ```csharp public class PaymentHandler { public static (Result, PaymentProcessed?, OrderShipped?, CustomerNotified?) Handle( ProcessPaymentCommand command, IPaymentService paymentService, IOrderService orderService) { var payment = paymentService.ProcessPayment(command.OrderId, command.Amount); if (payment.IsSuccessful) { var order = orderService.MarkAsPaid(command.OrderId); return ( payment, new PaymentProcessed(command.OrderId, command.Amount), order.IsReadyToShip ? new OrderShipped(command.OrderId) : null, new CustomerNotified(order.Email, "Payment Confirmed", "Thank you!") ); } return (Result.Failed("Payment failed"), null, null, null); } } ``` ### Saga Pattern Implementation ```csharp public class OrderSagaHandler { public static (Result, ReservationRequested?, PaymentRequested?) Handle( StartOrderSagaCommand command, ISagaRepository sagaRepo) { var saga = new OrderSaga { OrderId = command.OrderId, State = SagaState.Started }; sagaRepo.Save(saga); return ( Result.Success(), new ReservationRequested(command.OrderId, command.ProductId), new PaymentRequested(command.OrderId, command.Amount) ); } public static (Result, OrderCompleted?) Handle( ReservationConfirmed @event, ISagaRepository sagaRepo) { var saga = sagaRepo.GetByOrderId(@event.OrderId); saga.MarkReservationComplete(); if (saga.IsComplete) { return (Result.Success(), new OrderCompleted(@event.OrderId)); } return (Result.Success(), null); } } ``` ## Advanced Patterns ### Event Sourcing Integration ```csharp public class EventSourcedOrderHandler { public static (Result, params object[]) Handle( CreateOrderCommand command, IEventStore eventStore) { var events = new List { new OrderCreated(command.OrderId, command.Email), new InventoryReserved(command.ProductId, command.Quantity) }; // Add conditional events if (command.IsFirstOrder) { events.Add(new FirstOrderBonus(command.Email)); } var order = Order.FromEvents(events); eventStore.SaveEvents(command.OrderId, events); // Return order + all events for publishing return (order, events.ToArray()); } } ``` ### Batch Processing ```csharp public class BatchOrderHandler { public static (Result, params object[]) Handle( ProcessOrderBatchCommand command, IOrderRepository repository) { var orders = new List(); var events = new List(); foreach (var orderData in command.Orders) { var order = new Order(orderData); orders.Add(order); events.Add(new OrderCreated(order.Id, order.Email)); events.Add(new InventoryReserved(order.ProductId, order.Quantity)); } repository.SaveAll(orders); // Batch processing complete event events.Add(new BatchProcessingCompleted(command.BatchId, orders.Count)); return (orders.ToArray(), events.ToArray()); } } ``` ## Performance Considerations ### Inline vs Background Publishing Cascaded messages are published **inline** by default, meaning: * They execute in the same transaction/scope as the original handler * They can affect the response time of the original request * Failures in event handlers can impact the main operation ```csharp // This will execute all cascaded events before returning var result = await mediator.Invoke(new CreateOrderCommand("user@example.com")); ``` ### Async Event Handlers For better performance, make event handlers async: ```csharp public class EmailHandler { public static async Task Handle(CustomerNotified @event, IEmailService email) { // This runs asynchronously but still inline await email.SendAsync(@event.Email, @event.Subject, @event.Message); } } ``` ## Best Practices ### 1. Keep Events Small and Focused ```csharp // Good: Focused events public record OrderCreated(string OrderId, string Email); public record InventoryReserved(string ProductId, int Quantity); // Avoid: Large, multi-purpose events public record OrderEvent(Order Order, Customer Customer, Product Product, /* ... */); ``` ### 2. Use Nullable Types for Conditional Events ```csharp public static (Order, OrderCreated, WelcomeEmail?) Handle(CreateOrderCommand command) { var isNewCustomer = CheckNewCustomer(command.Email); return ( order, new OrderCreated(order.Id), isNewCustomer ? new WelcomeEmail(command.Email) : null // Conditional ); } ``` ### 3. Limit Cascade Depth ```csharp // Avoid deep cascading chains that are hard to follow // A -> B -> C -> D -> E -> F // Too deep! // Prefer: Flat event structures // A -> [B, C, D] // Better ``` ### 4. Handle Failures Gracefully ```csharp public static (Result, OrderCreated?) Handle(CreateOrderCommand command) { try { var order = CreateOrder(command); return (order, new OrderCreated(order.Id)); } catch (Exception ex) { return (Result.Failed(ex.Message), null); // No events on failure } } ``` ### 5. Consider Using Result Types ```csharp public static (Result, OrderCreated?, OrderFailure?) Handle(CreateOrderCommand command) { if (command.IsValid) { var order = CreateOrder(command); return (order, new OrderCreated(order.Id), null); } return ( Result.Invalid("Invalid order data"), null, new OrderFailure(command.Email, "Validation failed") ); } ``` ## Troubleshooting ### Debugging Cascaded Events Use structured logging to track event flow: ```csharp public class OrderHandler { public static (Order, OrderCreated, InventoryReserved) Handle( CreateOrderCommand command, ILogger logger) { logger.LogInformation("Processing order creation for {Email}", command.Email); var events = ( new Order(command), new OrderCreated(Guid.NewGuid().ToString(), command.Email), new InventoryReserved(command.ProductId, command.Quantity) ); logger.LogInformation("Order handler will cascade {EventCount} events", 2); return events; } } ``` ### Event Handler Registration Ensure all event handlers are discoverable: ```csharp // Make sure event handlers follow naming conventions public class InventoryHandler // Ends with 'Handler' { public static async Task Handle(InventoryReserved @event) // Method named 'Handle' { // Handler implementation } } ``` Cascading messages enable powerful event-driven architectures while maintaining clean, focused handler code. Use them to decouple business logic and create reactive systems that respond to domain events naturally. --- --- url: /guide/configuration.md --- # Configuration Options Foundatio Mediator provides two types of configuration: **compile-time configuration** via MSBuild properties that control source generator behavior, and **runtime configuration** via the `AddMediator()` method that controls mediator behavior. ## Compile-Time Configuration (MSBuild Properties) These properties control the source generator at compile time and affect code generation: ### Available MSBuild Properties ```xml Scoped true true ``` ### Property Details **`MediatorHandlerLifetime`** * **Values:** `Scoped`, `Transient`, `Singleton`, `None` * **Default:** `None` (handlers not auto-registered) * **Effect:** Automatically registers all discovered handlers with the specified DI lifetime * **Note:** When set to `None`, handlers are not automatically registered in DI **`MediatorDisableInterceptors`** * **Values:** `true`, `false` * **Default:** `false` * **Effect:** When `true`, disables C# interceptor generation and forces DI-based dispatch for all calls * **Use Case:** Debugging, cross-assembly calls, or when interceptors are not supported **`MediatorDisableOpenTelemetry`** * **Values:** `true`, `false` * **Default:** `false` * **Effect:** When `true`, disables OpenTelemetry integration code generation * **Use Case:** Reduce generated code size when telemetry is not needed ### Example .csproj Configuration ```xml net8.0 Scoped false true ``` ## Runtime Configuration (AddMediator Method) ### Default Setup The simplest configuration automatically discovers handlers and registers the mediator: ```csharp var builder = WebApplication.CreateBuilder(args); // Default configuration - discovers all handlers builder.Services.AddMediator(); var app = builder.Build(); ``` ### Configuration with Builder ```csharp builder.Services.AddMediator(cfg => cfg .AddAssembly(typeof(Program)) .SetMediatorLifetime(ServiceLifetime.Scoped) .UseForeachAwaitPublisher()); ``` ## Mediator Configuration Options ### MediatorConfiguration Class ```csharp public class MediatorConfiguration { public List? Assemblies { get; set; } public ServiceLifetime MediatorLifetime { get; set; } = ServiceLifetime.Scoped; public INotificationPublisher NotificationPublisher { get; set; } = new TaskWhenAllPublisher(); } ``` ## Handler Discovery Configuration ### Automatic Discovery By default, handlers are discovered automatically in the calling assembly: ```csharp // Discovers handlers in the current assembly builder.Services.AddMediator(); ``` ### Custom Assembly Discovery ```csharp builder.Services.AddMediator(cfg => cfg .AddAssembly(typeof(OrderHandler).Assembly) .AddAssembly(typeof(NotificationHandler).Assembly)); ``` ### Handler Registration Register handlers explicitly to control lifetime (otherwise first created instance is cached): ```csharp builder.Services.AddScoped(); builder.Services.AddTransient(); ``` ## MSBuild Configuration Disable interceptors if you need to force DI dispatch: ```xml true ``` ## Dependency Injection Integration `AddMediator` registers `IMediator` with configured lifetime and invokes generated handler module registration methods. It does not register handler classes; register them yourself to control lifetime. Custom mediator implementations can be supplied by registering your own `IMediator`. ## Environment-Specific Configuration Adjust registration or add middleware conditionally using standard ASP.NET Core environment checks; there are no built-in flags for tracing or throw-on-not-found. ## Logging Standard ASP.NET Core logging works; add logging middleware for per-message logs. ### Custom Logging Middleware ```csharp public class DetailedLoggingMiddleware { public static (DateTime StartTime, string CorrelationId) Before( object message, ILogger logger) { var correlationId = Guid.NewGuid().ToString("N")[..8]; var startTime = DateTime.UtcNow; logger.LogInformation( "[{CorrelationId}] Starting {MessageType} at {StartTime}", correlationId, message.GetType().Name, startTime); return (startTime, correlationId); } public static void After( object message, object? response, DateTime startTime, string correlationId, ILogger logger) { var duration = DateTime.UtcNow - startTime; logger.LogInformation( "[{CorrelationId}] Completed {MessageType} in {Duration}ms", correlationId, message.GetType().Name, duration.TotalMilliseconds); } } ``` --- --- url: /guide/dependency-injection.md --- # Dependency Injection Foundatio Mediator seamlessly integrates with Microsoft.Extensions.DependencyInjection to provide powerful dependency injection capabilities for both handlers and middleware. ## Registration Register the mediator and discover handlers in your DI container: ```csharp using Foundatio.Mediator; var builder = WebApplication.CreateBuilder(args); // Register the mediator - this automatically discovers and registers handlers builder.Services.AddMediator(); var app = builder.Build(); ``` ## Handler Lifetime Management ### Important: Handler Instances Are Cached When Not Registered If you don't explicitly register a handler in DI, the mediator will create an instance via `ActivatorUtilities.CreateInstance` and cache that instance (effectively singleton behavior). Constructor dependencies resolved in that first construction are reused for all invocations. Register handlers explicitly to control lifetime or rely on method parameter injection for per-invocation dependencies. ```csharp // WARNING: This handler is singleton - dependencies resolved once! public class OrderHandler { private readonly IOrderRepository _repository; // Resolved once, shared forever public OrderHandler(IOrderRepository repository) { _repository = repository; // This instance is reused for all requests! } public async Task> Handle(CreateOrderCommand command) { // If IOrderRepository is scoped (like DbContext), this will cause issues! return await _repository.CreateAsync(command.ToOrder()); } } ``` ### Automatic Handler Creation Resolution order: 1. **Registered in DI**: DI creates according to configured lifetime. 2. **Not registered**: Created once and cached (no DI lifetime scoping). ### Explicit Handler Registration for Lifetime Control To avoid singleton issues with scoped dependencies, register handlers explicitly: ```csharp builder.Services.AddMediator(); // Register handlers with proper lifetimes to match their dependencies builder.Services.AddScoped(); // Matches DbContext scope builder.Services.AddTransient(); // New instance each time builder.Services.AddSingleton(); // Truly singleton ``` ### Automatic Handler Registration with MSBuild You can automatically register all handlers in your project with a specific lifetime using the `MediatorHandlerLifetime` MSBuild property: ```xml Scoped ``` **Supported Values:** * `Scoped` - Handlers registered as scoped services * `Transient` - Handlers registered as transient services * `Singleton` - Handlers registered as singleton services **What this does:** * Automatically registers all discovered handlers with the specified lifetime * Eliminates the need for manual handler registration * Ensures consistent lifetime management across your application * Prevents singleton caching issues when using scoped dependencies **Example usage:** ```xml net8.0 Scoped ``` With this configuration, all your handlers will be automatically registered as scoped services: ```csharp // No manual registration needed - this handler is automatically scoped public class OrderHandler { private readonly IOrderRepository _repository; public OrderHandler(IOrderRepository repository) { _repository = repository; // Safe: both are scoped } public async Task> Handle(CreateOrderCommand command) { return await _repository.CreateAsync(command.ToOrder()); } } // Just register the mediator - handlers are auto-registered builder.Services.AddMediator(); builder.Services.AddScoped(); ``` ## Constructor Injection (Use with Caution) **⚠️ Note:** Constructor injection without DI registration leads to a cached singleton-like instance. ```csharp // PROBLEMATIC: Singleton handler with scoped dependency public class OrderHandler { private readonly IOrderRepository _repository; // DbContext-based repository public OrderHandler(IOrderRepository repository) { _repository = repository; // This DbContext instance lives forever! } public async Task> Handle(CreateOrderCommand command) { // This will eventually fail - DbContext disposed but handler keeps reference return await _repository.CreateAsync(command.ToOrder()); } } ``` **Solution:** Register the handler with appropriate lifetime: ```csharp // In Program.cs builder.Services.AddScoped(); builder.Services.AddScoped(); // Now handler matches repository lifetime // Handler is now properly scoped public class OrderHandler { private readonly IOrderRepository _repository; public OrderHandler(IOrderRepository repository) { _repository = repository; // Safe: both handler and repo are scoped } public async Task> Handle(CreateOrderCommand command) { return await _repository.CreateAsync(command.ToOrder()); } } ``` ## Method Parameter Injection (Recommended) **✅ Recommended:** Use method parameter injection to avoid singleton lifetime issues: ```csharp public class OrderHandler { // No constructor dependencies - handler can be singleton safely // First parameter is always the message // Additional parameters are resolved from DI per invocation public async Task> Handle( CreateOrderCommand command, // Message parameter IOrderRepository repository, // Fresh instance per call ILogger logger, // Fresh instance per call CancellationToken cancellationToken) // Automatically provided { logger.LogInformation("Processing order creation"); return await repository.CreateAsync(command.ToOrder(), cancellationToken); } } ``` ### Benefits of Method Parameter Injection 1. **No lifetime conflicts** - Dependencies resolved per invocation 2. **Automatic cancellation support** - `CancellationToken` provided automatically 3. **Cleaner testing** - Easy to mock individual method calls 4. **Better performance** - Handler can be singleton, dependencies fresh when needed ### Common Injectable Services These services are commonly injected into handler methods: * `ILogger` - For logging * `CancellationToken` - For cancellation support * `IServiceProvider` - For service location * Repository interfaces * Business service interfaces * Configuration objects ## Automatic DI Scope Management Foundatio.Mediator automatically manages dependency injection scopes to ensure proper lifetime handling of scoped services like DbContext. ### Root Handler Invocation Creates a Scope When you invoke a handler from a root mediator call (outside of another handler), a new DI scope is automatically created: ```csharp // This creates a new DI scope var result = await mediator.InvokeAsync(new CreateOrderCommand("test@example.com")); // The scope is disposed when the operation completes ``` ### Nested Operations Share the Same Scope All nested handler invocations within the same logical operation share the same DI scope: * **Cascading messages** - Events published via tuple returns use the same scope * **Manual handler calls** - Calling other handlers from within a handler * **Manual publishing** - Publishing events from within a handler * **Middleware operations** - All middleware in the pipeline ```csharp public class OrderHandler { public async Task<(Result, OrderCreated, EmailNotification)> Handle( CreateOrderCommand command, IOrderRepository repository, // Scoped - same instance throughout operation IMediator mediator, // Can call other handlers in same scope CancellationToken cancellationToken) { // This repository instance will be shared with all nested operations var order = await repository.CreateAsync(command.ToOrder(), cancellationToken); // These nested operations will use the SAME DI scope: // 1. Manual handler call await mediator.InvokeAsync(new UpdateInventoryCommand(order.ProductId), cancellationToken); // 2. Manual event publishing await mediator.PublishAsync(new OrderValidated(order.Id), cancellationToken); // 3. Cascading events (via tuple return) - also use same scope return ( Result.Created(order), new OrderCreated(order.Id, order.Email), // Uses same scope new EmailNotification(order.Email, "Order") // Uses same scope ); } } public class InventoryHandler { public async Task Handle( UpdateInventoryCommand command, IOrderRepository repository) // SAME INSTANCE as in OrderHandler! { // This shares the DbContext/repository instance with the parent handler var order = await repository.GetByIdAsync(command.OrderId); // ... update inventory } } ``` ### Benefits of Shared Scope **🔄 Consistent Data Access:** * All handlers in the same operation see the same data * DbContext change tracking works across nested handlers * Transactions can span multiple handlers **⚡ Performance:** * Expensive scoped services created once per operation * Connection pooling is more efficient * Reduced service resolution overhead **🛡️ Data Integrity:** * Natural unit of work boundaries * Easier to maintain consistency across operations * Proper cleanup when operation completes ## Middleware Lifetime Middleware instances are cached and reused by default: ```csharp public class LoggingMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; } public static void Before(object message, ILogger logger) { logger.LogInformation("Handling {MessageType}", message.GetType().Name); } } ``` ### Explicit Middleware Registration Control middleware lifetime by registering in DI: ```csharp builder.Services.AddMediator(); // Register middleware with specific lifetime builder.Services.AddScoped(); builder.Services.AddSingleton(); ``` ## Scoped Services Example Here's a complete example showing scoped services in a web application: ```csharp // Startup.cs or Program.cs builder.Services.AddMediator(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Handler using scoped services public class OrderHandler { public async Task> Handle( CreateOrderCommand command, IOrderRepository repository, IEmailService emailService, ILogger logger) { logger.LogInformation("Creating order for {Email}", command.Email); var order = new Order { Email = command.Email, Amount = command.Amount }; await repository.SaveAsync(order); await emailService.SendConfirmationAsync(order); return order; } } // Controller [ApiController] public class OrderController : ControllerBase { private readonly IMediator _mediator; public OrderController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task CreateOrder(CreateOrderCommand command) { var result = await _mediator.Invoke(command); return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Errors); } } ``` ## Service Location Pattern While constructor injection is preferred, you can access the service provider directly: ```csharp public class OrderHandler { public async Task> Handle( CreateOrderCommand command, IServiceProvider serviceProvider) { var repository = serviceProvider.GetRequiredService(); var logger = serviceProvider.GetRequiredService>(); logger.LogInformation("Creating order"); return await repository.CreateAsync(command.ToOrder()); } } ``` ## Best Practices ### 1. Prefer Method Injection for Most Scenarios ```csharp // ✅ RECOMMENDED: Method injection - no lifetime issues public class OrderHandler { public async Task> Handle( CreateOrderCommand command, IOrderRepository repository, // Fresh per call ILogger logger) // Fresh per call { logger.LogInformation("Creating order"); return await repository.CreateAsync(command.ToOrder()); } } ``` ### 2. Use Constructor Injection Only with Proper Registration ```csharp // ✅ SAFE: Constructor injection with explicit lifetime registration public class OrderHandler { private readonly IOrderRepository _repository; public OrderHandler(IOrderRepository repository) { _repository = repository; } public async Task> Handle(CreateOrderCommand command) { return await _repository.CreateAsync(command.ToOrder()); } } // Must register with matching lifetime: builder.Services.AddScoped(); builder.Services.AddScoped(); // Matches repository lifetime ``` ### 3. Static Methods Are Singleton-Safe ```csharp // ✅ EXCELLENT: Static methods with method injection public static class OrderHandler { public static async Task> Handle( CreateOrderCommand command, IOrderRepository repository, ILogger logger, CancellationToken cancellationToken) { logger.LogInformation("Creating order"); return await repository.CreateAsync(command.ToOrder(), cancellationToken); } } ``` ### 4. Avoid Service Location ```csharp // ❌ AVOID: Service location pattern public async Task Handle(CreateOrderCommand command, IServiceProvider provider) { var service = provider.GetService(); // Don't do this } // ✅ PREFER: Direct injection public async Task Handle(CreateOrderCommand command, IOrderService service) { // Use service directly } ``` ## Integration with ASP.NET Core The mediator integrates seamlessly with ASP.NET Core's built-in DI: ```csharp var builder = WebApplication.CreateBuilder(args); // Add framework services builder.Services.AddControllers(); builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); // Add application services builder.Services.AddScoped(); builder.Services.AddScoped(); // Add mediator - discovers handlers automatically builder.Services.AddMediator(); var app = builder.Build(); ``` This setup ensures that all your handlers have access to the same scoped services as your controllers, maintaining consistency across your application's request pipeline. --- --- url: /README.md --- # Foundatio Mediator Documentation This folder contains the VitePress v2.0 documentation for Foundatio Mediator. ## Development To run the documentation site locally: ```bash cd docs npm install npm run dev ``` The documentation will be available at `http://localhost:5173/` ## Building To build the documentation for production: ```bash npm run build ``` The built site will be in the `docs/.vitepress/dist` directory. ## Code Snippets The documentation uses VitePress code snippets to reference real code from the repository: ```markdown @[code{10-20}](../../samples/ConsoleSample/Handlers/Handlers.cs) ``` This ensures code examples stay up-to-date with the actual implementation. ## Configuration VitePress configuration is in `.vitepress/config.ts` with: * Navigation structure * Theme customization * Search configuration * Build optimization * Snippet handling ## Contributing When updating the documentation: 1. **Keep code snippets current** - Use the snippet feature to reference real code 2. **Test locally** - Always run `npm run dev` to verify changes 3. **Check navigation** - Ensure new pages are added to the sidebar 4. **Validate links** - Check internal links work correctly 5. **Update examples** - Keep examples practical and realistic ## Writing Guidelines * Use clear, concise language * Include practical examples for every concept * Reference real code from the samples when possible * Provide both basic and advanced usage patterns * Include performance considerations where relevant * Add "Next Steps" sections to guide readers --- --- url: /guide/getting-started.md --- # Getting Started Foundatio Mediator is a high-performance, convention-based mediator for .NET applications. This guide will walk you through the basic setup and your first handler. ## Installation Install the Foundatio.Mediator NuGet package: ::: code-group ```bash [.NET CLI] dotnet add package Foundatio.Mediator ``` ```xml [PackageReference] ``` ```powershell [Package Manager] Install-Package Foundatio.Mediator ``` ::: ## Basic Setup ### 1. Register the Mediator Add the mediator to your dependency injection container: ```csharp // Program.cs (Minimal API) var builder = WebApplication.CreateBuilder(args); builder.Services.AddMediator(); var app = builder.Build(); ``` ```csharp // Program.cs (Generic Host) var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddMediator(); }) .Build(); ``` ```csharp // Startup.cs (Traditional ASP.NET Core) public void ConfigureServices(IServiceCollection services) { services.AddMediator(); // ... other services } ``` ### 2. Create Your First Message Define a simple message: ```csharp public record Ping(string Text); ``` ### 3. Create a Handler Create a handler class following the naming conventions: ```csharp public static class PingHandler { public static string Handle(Ping msg) { return $"Pong: {msg.Text}"; } } ``` ### 4. Use the Mediator Inject and use the mediator in your application: ```csharp public class MyService { private readonly IMediator _mediator; public MyService(IMediator mediator) { _mediator = mediator; } public void DoSomething() { // Sync call - works when all handlers and middleware are sync var result = _mediator.Invoke(new Ping("Hello")); Console.WriteLine(result); // Output: "Pong: Hello" } public async Task DoSomethingAsync() { // Async call - works with both sync and async handlers var result = await _mediator.InvokeAsync(new Ping("Hello")); Console.WriteLine(result); // Output: "Pong: Hello" } } ``` ## Handler Conventions Foundatio Mediator uses simple naming conventions to discover handlers automatically: ### Class Names Handler classes must end with: * `Handler` * `Consumer` ### Method Names Valid handler method names: * `Handle` / `HandleAsync` * `Handles` / `HandlesAsync` * `Consume` / `ConsumeAsync` * `Consumes` / `ConsumesAsync` ### Method Signatures * **First parameter**: The message object (required) * **Remaining parameters**: Injected via dependency injection * **Return type**: Any type including `void`, `Task`, `Task` ## Examples ### Synchronous Handler ```csharp public record GetGreeting(string Name); public static class GreetingHandler { public static string Handle(GetGreeting query) { return $"Hello, {query.Name}!"; } } // Usage var greeting = mediator.Invoke(new GetGreeting("World")); ``` ### Asynchronous Handler ```csharp public record SendEmail(string To, string Subject, string Body); public class EmailHandler { public async Task HandleAsync(SendEmail command) { // Simulate sending email await Task.Delay(100); Console.WriteLine($"Email sent to {command.To}"); } } // Usage await mediator.InvokeAsync(new SendEmail("user@example.com", "Hello", "World")); ``` ### Handler with Dependency Injection ```csharp public class UserHandler { private readonly IUserRepository _repository; private readonly ILogger _logger; public UserHandler(IUserRepository repository, ILogger logger) { _repository = repository; _logger = logger; } public async Task HandleAsync(GetUser query, CancellationToken cancellationToken) { _logger.LogInformation("Getting user {UserId}", query.Id); return await _repository.GetByIdAsync(query.Id, cancellationToken); } } ``` ## Next Steps Now that you have the basics working, explore more advanced features: * [Handler Conventions](./handler-conventions) - Learn all the discovery rules * [Result Types](./result-types) - Using Result\ for robust error handling * [Middleware](./middleware) - Adding cross-cutting concerns * [Examples](../examples/simple-handlers) - See practical examples ## LLM-Friendly Documentation For AI assistants and Large Language Models, we provide optimized documentation formats: * [📜 LLMs Index](/llms.txt) - Quick reference with links to all sections * [📖 Complete Documentation](/llms-full.txt) - All docs in one LLM-friendly file These files follow the [llmstxt.org](https://llmstxt.org/) standard and contain the same information as this documentation in a format optimized for AI consumption. ## Common Issues ### Handler Not Found If you get a "handler not found" error: 1. Ensure your class name ends with `Handler` or `Consumer` 2. Ensure your method name follows the naming conventions 3. Ensure the first parameter matches your message type exactly --- --- url: /guide/handler-conventions.md --- # Handler Conventions Foundatio Mediator uses simple naming conventions to automatically discover handlers at compile time. This eliminates the need for interfaces, base classes, or manual registration while providing excellent compile-time validation. ## Class Naming Conventions Handler classes must end with one of these suffixes: * `Handler` * `Consumer` ```csharp // ✅ Valid handler class names public class UserHandler { } public class OrderHandler { } public class EmailConsumer { } public class NotificationConsumer { } // ❌ Invalid - won't be discovered public class UserService { } public class OrderProcessor { } ``` ## Method Naming Conventions Handler methods must use one of these names: * `Handle` / `HandleAsync` * `Handles` / `HandlesAsync` * `Consume` / `ConsumeAsync` * `Consumes` / `ConsumesAsync` ```csharp public class UserHandler { // ✅ All of these work public User Handle(GetUser query) { } public Task HandleAsync(GetUser query) { } public User Handles(GetUser query) { } public Task HandlesAsync(GetUser query) { } // ❌ These won't be discovered public User Process(GetUser query) { } public User Get(GetUser query) { } } ``` ## Method Signature Requirements ### First Parameter: The Message The first parameter must be the message object: ```csharp public class OrderHandler { // ✅ Message as first parameter public Order Handle(CreateOrder command) { } // ❌ Message not first public Order Handle(ILogger logger, CreateOrder command) { } } ``` ### Additional Parameters: Dependency Injection All parameters after the first are resolved via dependency injection: ```csharp public class OrderHandler { public async Task HandleAsync( CreateOrder command, // ✅ Message (required first) IOrderRepository repository, // ✅ Injected from DI ILogger logger, // ✅ Injected from DI CancellationToken ct // ✅ Automatically provided ) { logger.LogInformation("Creating order for {CustomerId}", command.CustomerId); return await repository.CreateAsync(command, ct); } } ``` ### Supported Parameter Types * **Any registered service** from the DI container * **CancellationToken** - automatically provided by the mediator * **Scoped services** - new instance per mediator invocation * **Singleton services** - shared instance ## Return Types Handlers can return any type: ```csharp public class ExampleHandler { // ✅ Void (fire-and-forget) public void Handle(LogMessage command) { } // ✅ Task (async fire-and-forget) public Task HandleAsync(SendEmail command) { } // ✅ Value types public int Handle(CalculateSum query) { } // ✅ Reference types public User Handle(GetUser query) { } // ✅ Generic types public Task> HandleAsync(GetOrders query) { } // ✅ Result types public Result Handle(GetUser query) { } // ✅ Tuples (for cascading messages) public (User user, UserCreated evt) Handle(CreateUser cmd) { } } ``` ## Handler Types ### Static Handlers Simple, stateless handlers can be static: ```csharp public static class CalculationHandler { public static int Handle(AddNumbers query) { return query.A + query.B; } public static decimal Handle(CalculateTax query) { return query.Amount * 0.08m; } } ``` **Benefits:** * No DI registration required (but can be registered when it is desired to control handler class lifetime) * Zero allocation for handler instance * Clear that no state is maintained ### Instance Handlers For handlers requiring dependencies: ```csharp public class UserHandler { private readonly IUserRepository _repository; private readonly ILogger _logger; // Constructor injection public UserHandler(IUserRepository repository, ILogger logger) { _repository = repository; _logger = logger; } public async Task HandleAsync(GetUser query) { _logger.LogInformation("Getting user {UserId}", query.Id); return await _repository.GetByIdAsync(query.Id); } } ``` **Note:** Handlers are singleton by default. Constructor dependencies are resolved once and shared across all invocations. ## Multiple Handlers in One Class A single class can handle multiple message types: ```csharp public class OrderHandler { public Result Handle(CreateOrder command) { } public Result Handle(GetOrder query) { } public Result Handle(UpdateOrder command) { } public Result Handle(DeleteOrder command) { } } ``` ## Handler Lifetime Management ### Default Behavior (Singleton) ```csharp public class UserHandler { private readonly ILogger _logger; // ⚠️ Resolved once, shared across all calls public UserHandler(ILogger logger) { _logger = logger; // Singleton dependency - OK } public User Handle(GetUser query, DbContext context) // ✅ Per-request dependency { // context is resolved fresh for each call return context.Users.Find(query.Id); } } ``` ### Explicit DI Registration Control handler lifetime by registering in DI: ```csharp // Scoped handlers (new instance per request) services.AddScoped(); services.AddScoped(); // Transient handlers (new instance per use) services.AddTransient(); ``` ### Automatic DI Registration Use MSBuild property to auto-register handlers: ```xml Scoped ``` Options: `None` (default), `Singleton`, `Scoped`, `Transient` ## Handler Discovery Rules ### Assembly Scanning The source generator scans the current assembly for: 1. **Public classes** ending with `Handler` or `Consumer` 2. **Public methods** with valid handler names 3. **First parameter** that defines the message type ### Manual Handler Discovery Handler classes can implement the `IFoundatioHandler` interface for manual discovery: ```csharp public class UserProcessor : IFoundatioHandler { public User Handle(GetUser query) { } // ✅ Discovered } ``` Handler classes and methods can be marked with the `[FoundatioHandler]` attribute for manual discovery: ```csharp public class UserProcessor { [FoundatioHandler] public User Process(GetUser query) { } // ✅ Discovered } ``` ## Compile-Time Validation The source generator provides compile-time errors for: ### Missing Handlers ```csharp // ❌ Compile-time error if no handler exists await mediator.InvokeAsync(new UnhandledMessage()); // Error: No handler found for message type 'UnhandledMessage' ``` ### Multiple Handlers (for Invoke) ```csharp public class Handler1 { public string Handle(DuplicateMessage msg) => "Handler1"; } public class Handler2 { public string Handle(DuplicateMessage msg) => "Handler2"; } // ❌ Compile-time error await mediator.InvokeAsync(new DuplicateMessage()); // Error: Multiple handlers found for message type 'DuplicateMessage' ``` ### Return Type Mismatches ```csharp public class UserHandler { public string Handle(GetUser query) => "Not a user"; // Returns string } // ❌ Compile-time error var user = await mediator.InvokeAsync(new GetUser(1)); // Error: Handler returns 'string' but expected 'User' ``` ### Async/Sync Mismatches ```csharp public class AsyncHandler { public async Task HandleAsync(GetMessage query) { await Task.Delay(100); return "Result"; } } // ❌ Compile-time error - handler is async but calling sync method var result = mediator.Invoke(new GetMessage()); // Error: Async handler found but sync method called ``` ## Ignoring Handlers Use `[FoundatioIgnore]` to exclude classes or methods: ```csharp [FoundatioIgnore] // Entire class ignored public class DisabledHandler { public string Handle(SomeMessage msg) => "Ignored"; } public class PartialHandler { public string Handle(Message1 msg) => "Handled"; [FoundatioIgnore] // Only this method ignored public string Handle(Message2 msg) => "Ignored"; } ``` ## Best Practices ### 1. Use Descriptive Handler Names ```csharp // ✅ Clear purpose public class UserRegistrationHandler { } public class OrderPaymentHandler { } public class EmailNotificationConsumer { } public class UserHandler { } // ❌ Too generic public class Handler { } // Handles what? ``` ### 2. Group Related Operations ```csharp // ✅ Cohesive handler public class OrderHandler { public Result Handle(CreateOrder cmd) { } public Result Handle(GetOrder query) { } public Result Handle(UpdateOrder cmd) { } public Result Handle(DeleteOrder cmd) { } } // ❌ Unrelated operations public class MixedHandler { public User Handle(GetUser query) { } public Order Handle(CreateOrder cmd) { } public Email Handle(SendEmail cmd) { } } ``` ### 3. Use Method Injection for Per-Request Dependencies ```csharp public class OrderHandler { private readonly ILogger _logger; // ✅ Singleton - safe for constructor public OrderHandler(ILogger logger) => _logger = logger; public async Task HandleAsync( CreateOrder command, DbContext context, // ✅ Scoped - use method injection ICurrentUser user, // ✅ Per-request - use method injection CancellationToken ct ) { // Fresh context and user for each request } } ``` ### 4. Keep Handlers Simple and Focused ```csharp // ✅ Single responsibility public class CreateOrderHandler { public async Task> HandleAsync(CreateOrder command) { // Only handles order creation } } // ❌ Too many responsibilities public class OrderHandler { public Result Handle(CreateOrder cmd) { /* ... */ } public Result Handle(UpdateInventory cmd) { /* ... */ } public Result Handle(SendEmail cmd) { /* ... */ } public Result Handle(ProcessPayment cmd) { /* ... */ } } ``` ## Next Steps * [Result Types](./result-types) - Robust error handling patterns * [Middleware](./middleware) - Cross-cutting concerns --- --- url: /guide/middleware.md --- # Middleware Middleware in Foundatio Mediator provides a powerful pipeline for implementing cross-cutting concerns like validation, logging, authorization, and error handling. Middleware can run before, after, and finally around handler execution. ## Basic Middleware Create middleware by following naming conventions: ```csharp public class LoggingMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; } public void Before(object message) { _logger.LogInformation("Handling {MessageType}", message.GetType().Name); } public void After(object message) { _logger.LogInformation("Handled {MessageType}", message.GetType().Name); } } ``` ## Middleware Conventions ### Class Names Middleware classes must end with `Middleware` ### Method Names Valid middleware method names: * `Before` / `BeforeAsync` * `After` / `AfterAsync` * `Finally` / `FinallyAsync` ### Method Parameters * **First parameter**: The message (can be `object`, interface, or concrete type) * **Remaining parameters**: Injected via DI (including `CancellationToken`) ## Lifecycle Methods ### Before Runs before the handler. Can return values that are passed to `After` and `Finally`: ```csharp public class TimingMiddleware { public Stopwatch Before(object message) { return Stopwatch.StartNew(); } public void Finally(object message, Stopwatch stopwatch) { stopwatch.Stop(); Console.WriteLine($"Handled {message.GetType().Name} in {stopwatch.ElapsedMilliseconds}ms"); } } ``` ### After Runs after successful handler completion. Only called if the handler succeeds: ```csharp public class AuditMiddleware { public void After(object message, IUserContext userContext) { _auditLog.Record($"User {userContext.UserId} executed {message.GetType().Name}"); } } ``` ### Finally Always runs, regardless of success or failure. Receives exception if handler failed: ```csharp public class ErrorHandlingMiddleware { public void Finally(object message, Exception? exception) { if (exception != null) { _errorLog.Record(message, exception); } } } ``` ## Short-Circuiting with HandlerResult Middleware can short-circuit handler execution by returning a `HandlerResult` from the `Before` method: ### Real-World Validation Example Let's look at the validation middleware from the sample: ```csharp public class ValidationMiddleware { public HandlerResult Before(object message) { if (!IsValid(message)) return Result.Invalid("Validation failed"); // Short-circuit return HandlerResult.Continue(); } } ``` ### Short-Circuit Usage ```csharp public class AuthorizationMiddleware { public HandlerResult Before(object message, IUserContext userContext) { if (!IsAuthorized(userContext, message)) { // Short-circuit with forbidden result return HandlerResult.ShortCircuit(Result.Forbidden("Access denied")); } // Continue to handler return HandlerResult.Continue(); } private bool IsAuthorized(IUserContext user, object message) { // Authorization logic return true; } } ``` ## State Passing Between Lifecycle Methods Values returned from `Before` are automatically injected into `After` and `Finally` by type: ```csharp public class TransactionMiddleware { public IDbTransaction Before(object message, IDbContext context) { return context.BeginTransaction(); } public async Task After(object message, IDbTransaction transaction) { await transaction.CommitAsync(); } public async Task Finally(object message, IDbTransaction transaction, Exception? ex) { if (ex != null) await transaction.RollbackAsync(); await transaction.DisposeAsync(); } } ``` ### Tuple State Returns You can return multiple values using tuples: ```csharp public class ComplexMiddleware { public (Stopwatch timer, string correlationId) Before(object message) { return (Stopwatch.StartNew(), Guid.NewGuid().ToString()); } public void Finally(object message, Stopwatch timer, string correlationId, Exception? ex) { timer.Stop(); _logger.LogInformation("Correlation {CorrelationId} completed in {Ms}ms", correlationId, timer.ElapsedMilliseconds); } } ``` ## Middleware Ordering Use the `[FoundatioOrder]` attribute to control execution order: ```csharp [FoundatioOrder(10)] public class ValidationMiddleware { // Runs early in Before, late in After/Finally } [FoundatioOrder(50)] public class LoggingMiddleware { // Runs later in Before, earlier in After/Finally } ``` **Default ordering** (without explicit order): 1. Message-specific middleware 2. Interface-based middleware 3. Object-based middleware **Execution flow**: * `Before`: Lower order values run first * `After`/`Finally`: Higher order values run first (reverse order for proper nesting) ## Message-Specific Middleware Target specific message types or interfaces: ```csharp // Only runs for ICommand messages public class CommandMiddleware { public void Before(ICommand command) { _commandLogger.Log($"Executing command: {command.GetType().Name}"); } } // Only runs for CreateOrder messages public class OrderCreationMiddleware { public HandlerResult Before(CreateOrder command) { if (_orderService.IsDuplicate(command)) return HandlerResult.ShortCircuit(Result.Conflict("Duplicate order")); return HandlerResult.Continue(); } } ``` ## Real-World Examples ### Comprehensive Logging Middleware Here's the logging middleware from the sample project: ```csharp public class LoggingMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; } public static void Before(object message, ILogger logger) { logger.LogInformation("Handling {MessageType}", message.GetType().Name); } } ``` ### Caching Middleware ```csharp public class CachingMiddleware { private readonly IMemoryCache _cache; public CachingMiddleware(IMemoryCache cache) => _cache = cache; public HandlerResult Before(IQuery query) { var cacheKey = $"{query.GetType().Name}:{GetQueryKey(query)}"; if (_cache.TryGetValue(cacheKey, out var cachedResult)) { return HandlerResult.ShortCircuit(cachedResult); } return HandlerResult.Continue(); } public void After(IQuery query, object result) { var cacheKey = $"{query.GetType().Name}:{GetQueryKey(query)}"; _cache.Set(cacheKey, result, TimeSpan.FromMinutes(5)); } } ``` ## Async Middleware All lifecycle methods support async versions: ```csharp public class AsyncMiddleware { public async Task BeforeAsync(object message, CancellationToken ct) { await SomeAsyncSetup(ct); return Stopwatch.StartNew(); } public async Task AfterAsync(object message, Stopwatch sw, CancellationToken ct) { await SomeAsyncCleanup(ct); } public async Task FinallyAsync(object message, Stopwatch sw, Exception? ex, CancellationToken ct) { sw.Stop(); await LogTiming(message, sw.ElapsedMilliseconds, ex, ct); } } ``` ## Middleware Registration Middleware are discovered automatically, but you can control their lifetime by registering them in DI: ```csharp // Singleton (default behavior) services.AddSingleton(); // Scoped (new instance per request) services.AddScoped(); // Transient (new instance per use) services.AddTransient(); ``` ## Ignoring Middleware Use `[FoundatioIgnore]` to exclude middleware classes or methods: ```csharp [FoundatioIgnore] // Entire class ignored public class DisabledMiddleware { public void Before(object message) { } } public class PartialMiddleware { public void Before(object message) { } [FoundatioIgnore] // Only this method ignored public void After(object message) { } } ``` ## Best Practices ### 1. Keep Middleware Focused Each middleware should handle one concern: ```csharp // ✅ Good - single responsibility public class ValidationMiddleware { } public class LoggingMiddleware { } public class AuthorizationMiddleware { } // ❌ Avoid - multiple responsibilities public class EverythingMiddleware { } ``` ### 2. Use Appropriate Lifecycle Methods ```csharp // ✅ Validation in Before (can short-circuit) public HandlerResult Before(object message) => ValidateMessage(message); // ✅ Logging in Finally (always runs) public void Finally(object message, Exception? ex) => LogResult(message, ex); // ❌ Don't validate in After (handler already ran) ``` ### 3. Handle Exceptions Gracefully ```csharp public void Finally(object message, Exception? exception) { if (exception != null) { // Log, notify, cleanup, etc. _logger.LogError(exception, "Handler failed for {MessageType}", message.GetType().Name); } } ``` ### 4. Use Strongly-Typed Message Parameters ```csharp // ✅ Specific to commands public void Before(ICommand command) { } // ✅ Specific to queries public void Before(IQuery query) { } // ⚠️ Generic (runs for everything) public void Before(object message) { } ``` ## Next Steps * [Validation Middleware Example](../examples/validation-middleware) - Complete validation implementation * [Handler Conventions](./handler-conventions) - Learn handler discovery rules --- --- url: /guide/performance.md --- # Performance & Interceptors Foundatio Mediator achieves blazing fast performance through C# interceptors and source generators, eliminating runtime reflection and providing near-direct call performance. ## How Interceptors Work C# interceptors are a compile-time feature that allows the mediator to replace method calls with direct, static method calls. This eliminates the overhead of traditional mediator patterns. ### Traditional Mediator Call Flow ```text Your Code → IMediator.Send() → Reflection → Handler Discovery → Handler Instantiation → Method Invoke ``` ### Foundatio Mediator with Interceptors ```text Your Code → [Intercepted] → Direct Static Method Call → Handler Method ``` ## Interceptor Generation The source generator automatically creates interceptor wrappers for handlers: ### Your Handler ```csharp public class OrderHandler { public static Result Handle(CreateOrderCommand command) { return new Order { Id = Guid.NewGuid(), Email = command.Email }; } } ``` ### Generated Interceptor (Simplified) ```csharp // Generated at compile time file static class GeneratedInterceptors { [InterceptsLocation("Program.cs", 15, 42)] // Intercepts specific call site public static Result InterceptCreateOrder(this IMediator mediator, CreateOrderCommand command) { // Direct call - no reflection! return OrderHandler.Handle(command); } } ``` ### Your Code ```csharp // This call gets intercepted at compile time var result = await mediator.Invoke(new CreateOrderCommand("user@example.com")); ``` ## Performance Benefits ### Benchmark Comparison ### Commands | Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct | |-------------------------------|-------------|-----------|-----------|--------|-----------|-----------| | **Direct\_Command** | **8.33 ns** | 0.17 ns | 0.24 ns | **-** | **0 B** | baseline | | **Foundatio\_Command** | **17.93 ns** | 0.36 ns | 0.34 ns | **-** | **0 B** | **2.15x** | | MediatR\_Command | 54.81 ns | 1.12 ns | 1.77 ns | 0.0038 | 192 B | 6.58x | | MassTransit\_Command | 1,585.85 ns | 19.82 ns | 17.57 ns | 0.0839 | 4232 B | 190.4x | ### Queries (Request/Response) | Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct | |-------------------------------|-------------|-----------|-----------|--------|-----------|-----------| | **Direct\_Query** | **32.12 ns** | 0.50 ns | 0.47 ns | 0.0038 | **192 B** | baseline | | **Foundatio\_Query** | **46.36 ns** | 0.94 ns | 0.84 ns | 0.0052 | **264 B** | **1.44x** | | MediatR\_Query | 81.40 ns | 1.32 ns | 1.23 ns | 0.0076 | 384 B | 2.53x | | MassTransit\_Query | 6,354.47 ns | 125.37 ns | 195.19 ns | 0.2518 | 12784 B | 197.8x | ### Events (Publish/Subscribe) | Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct | |-------------------------------|-------------|-----------|-----------|--------|-----------|-----------| | **Direct\_Event** | **8.12 ns** | 0.18 ns | 0.36 ns | **-** | **0 B** | baseline | | **Foundatio\_Publish** | **121.57 ns**| 0.80 ns | 0.71 ns | 0.0134 | **672 B** | **15.0x** | | MediatR\_Publish | 59.29 ns | 1.13 ns | 1.59 ns | 0.0057 | 288 B | 7.30x | | MassTransit\_Publish | 1,697.53 ns | 13.97 ns | 13.06 ns | 0.0877 | 4448 B | 209.0x | ### Dependency Injection Overhead | Method | Mean | Error | StdDev | Gen0 | Allocated | vs No DI | |---------------------------------------|-------------|-----------|-----------|--------|-----------|-----------| | **Direct\_QueryWithDependencies** | **39.24 ns** | 0.81 ns | 1.28 ns | 0.0052 | **264 B** | baseline | | **Foundatio\_QueryWithDependencies** | **53.30 ns** | 1.05 ns | 1.37 ns | 0.0067 | **336 B** | **1.36x** | | MediatR\_QueryWithDependencies | 79.97 ns | 0.54 ns | 0.51 ns | 0.0091 | 456 B | 2.04x | | MassTransit\_QueryWithDependencies | 5,397.69 ns | 61.05 ns | 50.98 ns | 0.2518 | 12857 B | 137.6x | ### Key Performance Insights * **🚀 Near-Optimal Performance**: Only slight overhead vs direct method calls * **⚡ Foundatio vs MediatR**: **3.06x faster** for commands, **1.76x faster** for queries * **🎯 Foundatio vs MassTransit**: **88x faster** for commands, **137x faster** for queries * **💾 Zero Allocation Commands**: Fire-and-forget operations have no GC pressure * **🔥 Minimal DI Overhead**: Only 36% performance cost for dependency injection * **📡 Efficient Publishing**: Event publishing scales well with multiple handlers *Benchmarks run on .NET 9.0 with BenchmarkDotNet. Results show Foundatio.Mediator achieves its design goal of getting as close as possible to direct method call performance.* ## Enabling/Disabling Interceptors ### Default Behavior Interceptors are **enabled by default** and provide the best performance. ### Disabling Interceptors You can disable interceptors via MSBuild property: ```xml true ``` When disabled, the mediator falls back to traditional DI-based handler resolution. ### Runtime vs Compile-time Behavior ```csharp // With interceptors (compile-time) var result = mediator.Invoke(command); // → Direct static call // Without interceptors (runtime) var result = mediator.Invoke(command); // → DI container lookup → reflection ``` ## Interceptor Limitations ### Same Assembly Requirement Interceptors only work for handlers in the **same assembly** as the mediator call: ```csharp // ✅ Works with interceptors - same assembly public class OrderController : ControllerBase { public async Task CreateOrder(CreateOrderCommand command) { // This gets intercepted if OrderHandler is in same assembly var result = await _mediator.Invoke(command); return Ok(result); } } public class OrderHandler // Same assembly { public static Result Handle(CreateOrderCommand command) => /* ... */; } ``` ```csharp // ❌ Falls back to DI - different assembly // If OrderHandler is in different assembly, uses DI container var result = await _mediator.Invoke(command); ``` ### Cross-Assembly Handler Resolution For cross-assembly scenarios, the mediator automatically falls back to DI-based resolution: ```csharp // Assembly A: Web.dll public class OrderController : ControllerBase { public async Task CreateOrder(CreateOrderCommand command) { // Falls back to DI if handler is in different assembly var result = await _mediator.Invoke(command); return Ok(result); } } // Assembly B: Handlers.dll public class OrderHandler { public static Result Handle(CreateOrderCommand command) => /* ... */; } ``` The DI registration generator ensures handlers are available: ```csharp // Generated registration services.AddKeyedTransient("CreateOrderCommand", new HandlerRegistration(typeof(CreateOrderCommand), typeof(OrderHandler), "Handle")); ``` ## Optimizing for Maximum Performance ### 1. Keep Handlers in Same Assembly ```csharp // Optimal structure - everything in same assembly MyApp.dll: ├── Controllers/ │ └── OrderController.cs ├── Handlers/ │ └── OrderHandler.cs └── Messages/ └── CreateOrderCommand.cs ``` ### 2. Use Static Handlers ```csharp // Faster - static method (no instance creation) public class OrderHandler { public static Result Handle(CreateOrderCommand command) { return CreateOrder(command); } } // Slower - instance method (requires DI instantiation) public class OrderHandler { public Result Handle(CreateOrderCommand command) { return CreateOrder(command); } } ``` ### 3. Minimize Dependencies in Static Handlers ```csharp // Optimal - minimal dependencies via parameters public static Result Handle( CreateOrderCommand command, IOrderRepository repository) // Injected only when needed { return repository.Create(command); } // Suboptimal - heavy constructor dependencies public class OrderHandler { private readonly IOrderRepository _repo; private readonly IEmailService _email; private readonly ILogger _logger; private readonly IEventBus _events; // ... many dependencies public OrderHandler(/* many constructor parameters */) { } } ``` ### 4. Batch Operations ```csharp // Efficient - batch multiple operations public static Result Handle(CreateOrderBatchCommand command) { return command.Orders.Select(CreateOrder).ToArray(); } // Inefficient - individual calls in loop foreach (var orderCommand in commands) { await mediator.Invoke(orderCommand); // Multiple interceptor calls } ``` ## Performance Monitoring ### Built-in Diagnostics The mediator includes built-in activity source for monitoring: ```csharp using var activity = MediatorActivitySource.StartActivity("Invoke"); activity?.SetTag("message.type", typeof(TMessage).Name); activity?.SetTag("handler.type", handlerType.Name); ``` ### Integration with Application Insights ```csharp builder.Services.AddApplicationInsightsTelemetry(); // Mediator calls will automatically appear in telemetry public class OrderController : ControllerBase { public async Task CreateOrder(CreateOrderCommand command) { // This call will be tracked in Application Insights var result = await _mediator.Invoke(command); return Ok(result); } } ``` ### Custom Performance Monitoring ```csharp public class PerformanceMiddleware { public static (Stopwatch Timer, string Operation) Before(object message) { var timer = Stopwatch.StartNew(); var operation = $"Handle{message.GetType().Name}"; return (timer, operation); } public static void After( object message, object? response, Stopwatch timer, string operation, ILogger logger) { timer.Stop(); if (timer.ElapsedMilliseconds > 100) // Log slow operations { logger.LogWarning("Slow operation {Operation} took {ElapsedMs}ms", operation, timer.ElapsedMilliseconds); } logger.LogDebug("Operation {Operation} completed in {ElapsedMs}ms", operation, timer.ElapsedMilliseconds); } } ``` ## Source Generator Performance ### Compilation Impact The source generators add minimal compilation overhead: * **Cold build**: +50-200ms (depending on project size) * **Incremental build**: +5-20ms * **Generated code size**: ~1-5KB per handler ### Generated Code Efficiency The generated interceptors are highly optimized: ```csharp // Minimal generated code - no abstractions [InterceptsLocation("Program.cs", 42, 15)] public static async Task> Intercept_CreateOrder( this IMediator mediator, CreateOrderCommand command, CancellationToken cancellationToken = default) { // Direct call with minimal overhead return await OrderHandler.Handle(command, cancellationToken); } ``` ## Memory Allocation Patterns ### Zero-Allocation Scenarios ```csharp // Zero allocations for simple handlers public static int Handle(GetCountQuery query) { return _cache.GetCount(); // Returns value type directly } // Zero allocations for value type messages public readonly record struct GetCountQuery; ``` ### Minimal Allocation Scenarios ```csharp // Minimal allocations - only for necessary objects public static Result Handle(CreateOrderCommand command) { return new Order { Email = command.Email }; // Only allocates Order } ``` ### Avoiding Allocations ```csharp // ❌ Avoid - creates unnecessary collections public static IEnumerable Handle(GetOrdersQuery query) { return orders.Where(o => o.Status == query.Status).ToList(); // Extra allocation } // ✅ Better - streaming results public static async IAsyncEnumerable Handle(GetOrdersQuery query) { await foreach (var order in GetOrdersAsync()) { if (order.Status == query.Status) yield return order; // No intermediate collections } } ``` ## Profiling and Benchmarking ### BenchmarkDotNet Integration ```csharp [MemoryDiagnoser] [SimpleJob(RuntimeMoniker.Net80)] public class MediatorBenchmarks { private IMediator _mediator; [GlobalSetup] public void Setup() { var services = new ServiceCollection(); services.AddMediator(); _mediator = services.BuildServiceProvider().GetRequiredService(); } [Benchmark] public async Task> InvokeCreateOrder() { return await _mediator.Invoke(new CreateOrderCommand("test@example.com")); } } ``` ### Performance Testing ```csharp [Fact] public async Task Should_Handle_High_Throughput() { var tasks = new List(); // Simulate 10,000 concurrent requests for (int i = 0; i < 10_000; i++) { tasks.Add(_mediator.Invoke(new CreateOrderCommand($"user{i}@example.com"))); } var stopwatch = Stopwatch.StartNew(); await Task.WhenAll(tasks); stopwatch.Stop(); // Should complete in reasonable time with minimal memory usage Assert.True(stopwatch.ElapsedMilliseconds < 5000); } ``` ## Best Practices for Performance ### 1. Design for Interceptors ```csharp // ✅ Interceptor-friendly - same assembly, static method public class OrderHandler { public static Result Handle(CreateOrderCommand command) { return CreateOrder(command); } } ``` ### 2. Use Async Appropriately ```csharp // ✅ Async when doing I/O public static async Task> Handle(CreateOrderCommand command, IRepository repo) { return await repo.CreateAsync(command.ToOrder()); } // ✅ Sync when no I/O public static Result Handle(CreateOrderCommand command) { return new Order { Email = command.Email }; } ``` ### 3. Optimize Message Design ```csharp // ✅ Lightweight messages public readonly record struct GetCountQuery; public record CreateOrderCommand(string Email, decimal Amount); // ❌ Heavy messages public class CreateOrderCommand { public Customer Customer { get; set; } public Product[] Products { get; set; } public ShippingInfo Shipping { get; set; } // ... large object graph } ``` ### 4. Leverage Streaming for Large Data ```csharp // ✅ Stream large result sets public static async IAsyncEnumerable Handle(GetAllOrdersQuery query) { await foreach (var order in repository.GetOrdersStreamAsync()) yield return order; } // ❌ Load everything into memory public static Task Handle(GetAllOrdersQuery query) { return repository.GetAllOrdersAsync(); // Could be huge! } ``` Foundatio Mediator's interceptor-based approach provides exceptional performance while maintaining clean, maintainable code. By understanding how interceptors work and following performance best practices, you can build highly efficient applications that scale to handle millions of requests. --- --- url: /guide/result-types.md --- # Result Types Foundatio Mediator includes built-in `Result` and `Result` types for robust error handling without relying on exceptions. These discriminated union types capture success, validation errors, conflicts, not found states, and more. ## Why Result Types? Traditional .NET applications often use exceptions for control flow, which can be expensive and make it difficult to handle expected error conditions gracefully. Result types provide a better alternative: * **Performance**: No exception overhead for expected failures * **Explicit**: Return types clearly indicate potential failure modes * **Composable**: Easy to chain operations and handle errors functionally * **Testable**: Straightforward to test all code paths ## Basic Result Usage ### Result (Non-Generic) For operations that don't return data but can fail: ```csharp public Result Handle(DeleteOrder command) { if (!_orders.ContainsKey(command.OrderId)) return Result.NotFound($"Order {command.OrderId} not found"); _orders.Remove(command.OrderId); return Result.NoContent(); // Success with no content } ``` ### Result\ (Generic) For operations that return data or can fail: ```csharp public Result Handle(GetOrder query) { if (!_orders.TryGetValue(query.OrderId, out var order)) return Result.NotFound($"Order {query.OrderId} not found"); return order; // Implicit conversion to Result } ``` ## Result Status Types Result types include several built-in status types: ```csharp public enum ResultStatus { Ok, Created, NoContent, BadRequest, Error, Invalid, NotFound, Unauthorized, Forbidden, Conflict, CriticalError, Unavailable } ``` ## Creating Results ### Success Results ```csharp // Simple success return Result.Success(); return Result.Success(user); // Created with location return Result.Created(order, $"/orders/{order.Id}"); // No content (for deletions) return Result.NoContent(); // Implicit conversion from value return user; // Automatically becomes Result.Success(user) ``` ### Error Results ```csharp // Not found return Result.NotFound("User not found"); return Result.NotFound($"Order {orderId} not found"); // Validation errors return Result.Invalid("Name is required"); return Result.Invalid(validationErrors); // Forbidden access return Result.Forbidden("Insufficient permissions"); // Conflict (e.g., duplicate key) return Result.Conflict("Email already exists"); // Generic error return Result.Error("Something went wrong"); ``` ## Working with Results ### Checking Success ```csharp var result = await mediator.InvokeAsync>(new GetUser(123)); if (result.IsSuccess) { var user = result.Value; Console.WriteLine($"Found user: {user.Name}"); } else { Console.WriteLine($"Error: {result.ErrorMessage}"); } ``` ### Pattern Matching ```csharp var result = await mediator.InvokeAsync>(new GetOrder("123")); var message = result.Status switch { ResultStatus.Ok => $"Order: {result.Value.Description}", ResultStatus.NotFound => "Order not found", ResultStatus.Forbidden => "Access denied", _ => $"Error: {result.ErrorMessage}" }; ``` ### Accessing Properties ```csharp public class Result { public bool IsSuccess { get; } public bool IsFailure => !IsSuccess; public ResultStatus Status { get; } public T Value { get; } public string? ErrorMessage { get; } public IReadOnlyList ValidationErrors { get; } } ``` ## Validation Errors Result types support detailed validation errors: ```csharp public Result Handle(CreateUser command) { var errors = new List(); if (string.IsNullOrEmpty(command.Name)) errors.Add(new ValidationError("Name", "Name is required")); if (command.Age < 0) errors.Add(new ValidationError("Age", "Age must be positive")); if (errors.Any()) return Result.Invalid(errors); var user = new User(command.Name, command.Age); return user; } ``` ## Integration with ASP.NET Core Result types work seamlessly with ASP.NET Core controllers: ```csharp [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IMediator _mediator; public OrdersController(IMediator mediator) => _mediator = mediator; [HttpGet("{id}")] public async Task> GetOrder(string id) { var result = await _mediator.InvokeAsync>(new GetOrder(id)); return result.Status switch { ResultStatus.Ok => Ok(result.Value), ResultStatus.NotFound => NotFound(result.ErrorMessage), ResultStatus.Forbidden => Forbid(), _ => BadRequest(result.ErrorMessage) }; } [HttpPost] public async Task> CreateOrder(CreateOrder command) { var result = await _mediator.InvokeAsync>(command); return result.Status switch { ResultStatus.Created => CreatedAtAction(nameof(GetOrder), new { id = result.Value.Id }, result.Value), ResultStatus.Invalid => BadRequest(result.ValidationErrors), ResultStatus.Conflict => Conflict(result.ErrorMessage), _ => BadRequest(result.ErrorMessage) }; } } ``` ## Extension Methods You can create extension methods to make Result handling more convenient: ```csharp public static class ResultExtensions { public static ActionResult ToActionResult(this Result result) { return result.Status switch { ResultStatus.Ok => new OkObjectResult(result.Value), ResultStatus.Created => new CreatedResult("", result.Value), ResultStatus.NoContent => new NoContentResult(), ResultStatus.NotFound => new NotFoundObjectResult(result.ErrorMessage), ResultStatus.Invalid => new BadRequestObjectResult(result.ValidationErrors), ResultStatus.Forbidden => new ForbidResult(), ResultStatus.Conflict => new ConflictObjectResult(result.ErrorMessage), _ => new BadRequestObjectResult(result.ErrorMessage) }; } } // Usage [HttpGet("{id}")] public async Task GetOrder(string id) { var result = await _mediator.InvokeAsync>(new GetOrder(id)); return result.ToActionResult(); } ``` ## Best Practices ### 1. Be Specific with Error Messages ```csharp // ❌ Generic return Result.NotFound("Not found"); // ✅ Specific return Result.NotFound($"Order {orderId} not found"); ``` ### 2. Use Appropriate Status Codes ```csharp // For business rule violations return Result.Conflict("Cannot delete order with pending payments"); // For authorization failures return Result.Forbidden("User cannot access other users' orders"); // For validation failures return Result.Invalid("Email format is invalid"); ``` ### 3. Handle All Result Cases ```csharp // ❌ Only checking IsSuccess if (result.IsSuccess) return result.Value; // What about errors? // ✅ Pattern matching all cases return result.Status switch { ResultStatus.Ok => result.Value, ResultStatus.NotFound => throw new NotFoundException(result.ErrorMessage), _ => throw new InvalidOperationException(result.ErrorMessage) }; ``` ### 4. Compose Results ```csharp public async Task> Handle(GetOrderSummary query) { var orderResult = await _mediator.InvokeAsync>(new GetOrder(query.OrderId)); if (orderResult.IsFailure) return Result.FromResult(orderResult); var customerResult = await _mediator.InvokeAsync>(new GetCustomer(orderResult.Value.CustomerId)); if (customerResult.IsFailure) return Result.FromResult(customerResult); var summary = new OrderSummary(orderResult.Value, customerResult.Value); return summary; } ``` ## Next Steps * [CRUD Operations](/examples/crud-operations) - See Result types in action * [Validation Middleware](/examples/validation-middleware) - Automatic validation with Results * [Handler Conventions](/guide/handler-conventions) - Learn handler return type rules --- --- url: /guide/streaming-handlers.md --- # Streaming Handlers Foundatio Mediator supports streaming handlers that can return `IAsyncEnumerable` for scenarios where you need to process and return data incrementally, such as large datasets, real-time feeds, or progressive data processing. ## Basic Streaming Handler ```csharp public class ProductStreamHandler { public static async IAsyncEnumerable Handle( GetProductsStreamQuery query, IProductRepository repository, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var product in repository.GetProductsAsync(query.CategoryId, cancellationToken)) { // Process each product before yielding product.CalculateDiscountPrice(); yield return product; } } } ``` ## Consuming Streaming Results ### Basic Consumption ```csharp public class ProductController : ControllerBase { private readonly IMediator _mediator; public ProductController(IMediator mediator) { _mediator = mediator; } [HttpGet("stream")] public async IAsyncEnumerable GetProductsStream( [FromQuery] string categoryId, CancellationToken cancellationToken) { var query = new GetProductsStreamQuery(categoryId); await foreach (var product in _mediator.Invoke>(query, cancellationToken)) { yield return product; } } } ``` ### Processing with LINQ ```csharp public async Task ProcessProductsAsync() { var query = new GetProductsStreamQuery("electronics"); await foreach (var product in _mediator.Invoke>(query) .Where(p => p.Price > 100) .Take(50)) { await ProcessProductAsync(product); } } ``` ## Real-World Examples ### Large Dataset Processing ```csharp public record GetOrdersStreamQuery(DateTime StartDate, DateTime EndDate, int BatchSize = 100); public class OrderStreamHandler { public static async IAsyncEnumerable Handle( GetOrdersStreamQuery query, IOrderRepository repository, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken = default) { logger.LogInformation("Starting order stream for period {Start} to {End}", query.StartDate, query.EndDate); var totalProcessed = 0; await foreach (var batch in repository.GetOrderBatchesAsync( query.StartDate, query.EndDate, query.BatchSize, cancellationToken)) { foreach (var order in batch) { var summary = new OrderSummary { Id = order.Id, CustomerEmail = order.CustomerEmail, Total = order.Total, Status = order.Status, ProcessedAt = DateTime.UtcNow }; totalProcessed++; if (totalProcessed % 1000 == 0) { logger.LogInformation("Processed {Count} orders", totalProcessed); } yield return summary; } } logger.LogInformation("Completed order stream. Total processed: {Total}", totalProcessed); } } ``` ### Real-Time Data Feed ```csharp public record GetLiveStockPricesQuery(string[] Symbols); public class StockPriceStreamHandler { public static async IAsyncEnumerable Handle( GetLiveStockPricesQuery query, IStockPriceService stockService, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Subscribe to real-time stock price updates await foreach (var priceUpdate in stockService.SubscribeToSymbols(query.Symbols, cancellationToken)) { var stockPrice = new StockPrice { Symbol = priceUpdate.Symbol, Price = priceUpdate.CurrentPrice, Change = priceUpdate.PriceChange, Volume = priceUpdate.Volume, Timestamp = priceUpdate.Timestamp }; yield return stockPrice; } } } ``` ### File Processing ```csharp public record ProcessCsvFileQuery(string FilePath); public class CsvProcessorHandler { public static async IAsyncEnumerable Handle( ProcessCsvFileQuery query, ICsvParser csvParser, IValidator validator, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var line in csvParser.ReadLinesAsync(query.FilePath, cancellationToken)) { if (csvParser.TryParseCustomer(line, out var customer)) { var validationResult = await validator.ValidateAsync(customer, cancellationToken); if (validationResult.IsValid) { yield return customer; } else { // Could yield error records or log validation failures await LogValidationErrorAsync(line, validationResult.Errors); } } } } } ``` ## Advanced Streaming Patterns ### Streaming with Transformation ```csharp public class DataTransformStreamHandler { public static async IAsyncEnumerable Handle( TransformDataStreamQuery query, IDataSource dataSource, ITransformationService transformer, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var rawData in dataSource.GetDataStreamAsync(query.Filter, cancellationToken)) { // Apply transformations var transformed = await transformer.TransformAsync(rawData, cancellationToken); // Apply business rules if (transformer.MeetsBusinessCriteria(transformed)) { yield return transformed; } } } } ``` ### Streaming with Aggregation ```csharp public class SalesReportStreamHandler { public static async IAsyncEnumerable Handle( GenerateSalesReportQuery query, ISalesRepository repository, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var currentPeriod = query.StartDate; while (currentPeriod <= query.EndDate) { var endPeriod = currentPeriod.AddDays(query.PeriodDays); var sales = await repository.GetSalesForPeriodAsync(currentPeriod, endPeriod, cancellationToken); var metrics = new SalesMetrics { Period = currentPeriod, TotalSales = sales.Sum(s => s.Amount), OrderCount = sales.Count(), AverageOrderValue = sales.Average(s => s.Amount), TopProduct = sales.GroupBy(s => s.ProductId) .OrderByDescending(g => g.Sum(s => s.Amount)) .First().Key }; yield return metrics; currentPeriod = endPeriod; } } } ``` ### Conditional Streaming ```csharp public class ConditionalStreamHandler { public static async IAsyncEnumerable Handle( ProcessItemsQuery query, IItemRepository repository, IBusinessRuleEngine ruleEngine, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var item in repository.GetItemsAsync(query.Filter, cancellationToken)) { // Apply business rules to determine if item should be processed var ruleResult = await ruleEngine.EvaluateAsync(item, cancellationToken); if (ruleResult.ShouldProcess) { var processed = new ProcessedItem { Id = item.Id, Data = item.Data, ProcessingRules = ruleResult.AppliedRules, ProcessedAt = DateTime.UtcNow }; // Additional conditional logic if (ruleResult.RequiresEnrichment) { processed = await EnrichItemAsync(processed, cancellationToken); } yield return processed; } } } } ``` ## Error Handling in Streams ### Graceful Error Recovery ```csharp public class RobustStreamHandler { public static async IAsyncEnumerable> Handle( ProcessDataStreamQuery query, IDataProcessor processor, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var item in GetDataStreamAsync(query, cancellationToken)) { Result result; try { var processed = await processor.ProcessAsync(item, cancellationToken); result = Result.Success(processed); } catch (ProcessingException ex) { logger.LogWarning(ex, "Failed to process item {ItemId}", item.Id); result = Result.Failed($"Processing failed: {ex.Message}"); } catch (Exception ex) { logger.LogError(ex, "Unexpected error processing item {ItemId}", item.Id); result = Result.Failed("Unexpected processing error"); } yield return result; } } } ``` ### Circuit Breaker Pattern ```csharp public class CircuitBreakerStreamHandler { private static int _consecutiveFailures = 0; private const int MaxFailures = 5; public static async IAsyncEnumerable Handle( ProcessStreamQuery query, IExternalService externalService, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var item in GetItemsAsync(query, cancellationToken)) { if (_consecutiveFailures >= MaxFailures) { throw new InvalidOperationException("Circuit breaker is open due to consecutive failures"); } try { var result = await externalService.ProcessAsync(item, cancellationToken); _consecutiveFailures = 0; // Reset on success yield return result; } catch (Exception) { _consecutiveFailures++; throw; } } } } ``` ## Performance Considerations ### Buffering for Better Performance ```csharp public class BufferedStreamHandler { public static async IAsyncEnumerable Handle( ProcessBatchStreamQuery query, IDataSource dataSource, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var buffer = new List(query.BatchSize); await foreach (var item in dataSource.GetDataAsync(cancellationToken)) { buffer.Add(item); if (buffer.Count >= query.BatchSize) { var batch = await ProcessBatchAsync(buffer, cancellationToken); yield return batch; buffer.Clear(); } } // Process remaining items if (buffer.Count > 0) { var finalBatch = await ProcessBatchAsync(buffer, cancellationToken); yield return finalBatch; } } } ``` ### Memory-Efficient Processing ```csharp public class MemoryEfficientHandler { public static async IAsyncEnumerable Handle( ProcessLargeFileQuery query, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var fileStream = new FileStream(query.FilePath, FileMode.Open, FileAccess.Read); using var reader = new StreamReader(fileStream); string? line; while ((line = await reader.ReadLineAsync()) != null) { if (cancellationToken.IsCancellationRequested) yield break; // Process line without loading entire file into memory var processed = ProcessLine(line); if (!string.IsNullOrEmpty(processed)) yield return processed; } } } ``` ## Integration with ASP.NET Core ### Streaming API Endpoints ```csharp [ApiController] [Route("api/[controller]")] public class DataController : ControllerBase { private readonly IMediator _mediator; public DataController(IMediator mediator) { _mediator = mediator; } [HttpGet("stream")] public async IAsyncEnumerable GetDataStream( [FromQuery] string filter, CancellationToken cancellationToken) { var query = new GetDataStreamQuery(filter); await foreach (var item in _mediator.Invoke>(query, cancellationToken)) { yield return item; } } [HttpGet("csv")] public async Task ExportToCsv( [FromQuery] string filter, CancellationToken cancellationToken) { var query = new GetDataStreamQuery(filter); Response.Headers.Add("Content-Type", "text/csv"); Response.Headers.Add("Content-Disposition", "attachment; filename=data.csv"); await foreach (var item in _mediator.Invoke>(query, cancellationToken)) { var csv = $"{item.Id},{item.Name},{item.Value}\n"; await Response.WriteAsync(csv, cancellationToken); } return new EmptyResult(); } } ``` ### SignalR Integration ```csharp public class LiveDataHub : Hub { private readonly IMediator _mediator; public LiveDataHub(IMediator mediator) { _mediator = mediator; } public async Task SubscribeToData(string filter) { var query = new GetLiveDataStreamQuery(filter); await foreach (var data in _mediator.Invoke>(query, Context.ConnectionAborted)) { await Clients.Caller.SendAsync("DataUpdate", data, Context.ConnectionAborted); } } } ``` ## Best Practices ### 1. Always Use CancellationToken ```csharp public static async IAsyncEnumerable Handle( StreamQuery query, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Always check for cancellation if (cancellationToken.IsCancellationRequested) yield break; // Pass cancellation token to async operations await foreach (var item in source.GetItemsAsync(cancellationToken)) { yield return item; } } ``` ### 2. Handle Backpressure ```csharp public static async IAsyncEnumerable Handle( StreamQuery query, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var semaphore = new SemaphoreSlim(Environment.ProcessorCount); await foreach (var item in source.GetItemsAsync(cancellationToken)) { await semaphore.WaitAsync(cancellationToken); try { var processed = await ProcessItemAsync(item, cancellationToken); yield return processed; } finally { semaphore.Release(); } } } ``` ### 3. Provide Progress Reporting ```csharp public static async IAsyncEnumerable> Handle( StreamQuery query, IProgress? progress = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var totalItems = await GetTotalItemCountAsync(query); var processedCount = 0; await foreach (var item in source.GetItemsAsync(cancellationToken)) { var result = await ProcessItemAsync(item, cancellationToken); processedCount++; progress?.Report(new ProcessingProgress(processedCount, totalItems)); yield return new ProcessingResult(result, processedCount, totalItems); } } ``` ### 4. Implement Proper Cleanup ```csharp public static async IAsyncEnumerable Handle( StreamQuery query, [EnumeratorCancellation] CancellationToken cancellationToken = default) { IDisposable? resource = null; try { resource = await AcquireResourceAsync(); await foreach (var item in ProcessWithResourceAsync(resource, query, cancellationToken)) { yield return item; } } finally { resource?.Dispose(); } } ``` Streaming handlers are powerful for processing large datasets, real-time data feeds, and scenarios where you need to return results incrementally. They provide excellent memory efficiency and allow for responsive, scalable applications. --- --- url: /guide/what-is-foundatio-mediator.md --- # What is Foundatio Mediator? Foundatio Mediator is a high-performance, convention-based mediator pattern implementation for .NET applications. It leverages modern C# features like source generators and interceptors to deliver near-direct call performance while maintaining the benefits of the mediator pattern. ## What is the Mediator Pattern? The mediator pattern defines how a set of objects interact with each other. Instead of objects communicating directly, they communicate through a central mediator. This promotes loose coupling and makes your code more maintainable and testable. ```mermaid graph TD A[Controller] --> M[Mediator] B[Service] --> M C[Background Job] --> M M --> H1[User Handler] M --> H2[Order Handler] M --> H3[Email Handler] M --> MW[Middleware] ``` ## Key Benefits ### 🚀 Exceptional Performance Foundatio Mediator uses **C# interceptors** to transform mediator calls into direct method calls at compile time: ```csharp // You write this: await mediator.InvokeAsync(new GetUser(123)); // The generator transforms it to essentially: await UserHandler_Generated.HandleAsync(new GetUser(123), serviceProvider, cancellationToken); ``` This results in performance that's **2-15x faster** than other mediator implementations and very close to direct method call performance. ### ⚡ Convention-Based Discovery No interfaces or base classes required. Just follow simple naming conventions: ```csharp // ✅ This works - class ends with "Handler" public class UserHandler { // ✅ Method named "Handle" or "HandleAsync" public User Handle(GetUser query) { /* ... */ } } // ✅ This also works - static methods public static class OrderHandler { public static async Task HandleAsync(CreateOrder cmd) { /* ... */ } } ``` ### 🔧 Seamless Dependency Injection Full support for Microsoft.Extensions.DependencyInjection with both constructor and method injection: ```csharp public class UserHandler { // Constructor injection for long-lived dependencies public UserHandler(ILogger logger) { /* ... */ } // Method injection for per-request dependencies public async Task HandleAsync( GetUser query, IUserRepository repo, // Injected from DI CancellationToken ct // Automatically provided ) { /* ... */ } } ``` ### 🎯 Rich Result Types Built-in `Result` discriminated union for robust error handling without exceptions: ```csharp public Result Handle(GetUser query) { var user = _repository.FindById(query.Id); if (user == null) return Result.NotFound($"User {query.Id} not found"); if (!user.IsActive) return Result.Forbidden("User account is disabled"); return user; // Implicit conversion to Result } ``` ### 🎪 Powerful Middleware Pipeline Cross-cutting concerns made easy with Before/After/Finally hooks: ```csharp public class ValidationMiddleware { public HandlerResult Before(object message) { if (!IsValid(message)) return Result.Invalid("Validation failed"); // Short-circuit return HandlerResult.Continue(); } } public class LoggingMiddleware { public Stopwatch Before(object message) => Stopwatch.StartNew(); public void Finally(object message, Stopwatch sw, Exception? ex) { _logger.LogInformation("Handled {MessageType} in {Ms}ms", message.GetType().Name, sw.ElapsedMilliseconds); } } ``` ## When to Use Foundatio Mediator ### ✅ Great For * **Clean Architecture** applications with command/query separation * **Microservices** with clear request/response boundaries * **Event-driven** architectures with publish/subscribe patterns * **High-performance** scenarios where every nanosecond matters * **Large teams** needing consistent patterns and conventions * **Testing** scenarios requiring isolated, mockable handlers ### ⚠️ Consider Alternatives For * **Simple CRUD** applications with minimal business logic * **Performance-critical** inner loops where even 10ns matters * **Legacy codebases** that can't adopt modern .NET features * **Teams resistant** to convention-based approaches ## Next Steps Ready to get started? Here's what to explore next: * [Getting Started](./getting-started) - Set up your first handler * [Handler Conventions](./handler-conventions) - Learn the discovery rules * [Examples](../examples/simple-handlers) - See practical implementations --- --- url: /guide/why-foundatio-mediator.md --- # Why Choose Foundatio Mediator? Foundatio Mediator stands out from other mediator implementations by combining exceptional performance with developer-friendly conventions. Here's why it might be the right choice for your project. ## Performance That Matters ### Near-Direct Call Performance Unlike other mediator libraries that rely on runtime reflection or complex dispatch mechanisms, Foundatio Mediator uses **C# interceptors** to transform your mediator calls into essentially direct method calls: ```csharp // You write this: await mediator.InvokeAsync(new GetUser(123)); // The source generator creates something like this: await UserHandler_Generated.HandleAsync(new GetUser(123), serviceProvider, cancellationToken); ``` This results in: * **2-15x faster** than other mediator libraries * **Zero allocations** for fire-and-forget commands * **No reflection overhead** at runtime * **Full compiler optimizations** including inlining ### Benchmark Results Benchmark highlights (see root README for full tables): | Scenario | Foundatio | MediatR | MassTransit | |----------|-----------|---------|------------| | Commands | 17.93 ns (2.15x direct) | 54.81 ns | 1,585.85 ns | | Queries | 46.36 ns (1.44x direct) | 81.40 ns | 6,354.47 ns | | Events (publish) | 121.57 ns | 59.29 ns | 1,697.53 ns | Event publishing involves multiple handler pipeline steps; Foundatio optimizes single-handler command/query paths for near-direct performance. ## Developer Experience ### Convention Over Configuration No interfaces, base classes, or complex registration required: ```csharp // ✅ Foundatio Mediator - Just naming conventions public class UserHandler { public User Handle(GetUser query) => _repository.Find(query.Id); } // ❌ Other libraries - Interfaces required public class UserHandler : IRequestHandler { public User Handle(GetUser query) => _repository.Find(query.Id); } ``` ### Rich Error Handling Built-in `Result` types eliminate exception-driven control flow: ```csharp public Result Handle(GetUser query) { var user = _repository.Find(query.Id); if (user == null) return Result.NotFound($"User {query.Id} not found"); if (!user.IsActive) return Result.Forbidden("Account disabled"); return user; // Implicit conversion } ``` Compare this to exception-heavy alternatives: ```csharp // ❌ Exception-based approach public User Handle(GetUser query) { var user = _repository.Find(query.Id); if (user == null) throw new NotFoundException($"User {query.Id} not found"); if (!user.IsActive) throw new UnauthorizedException("Account disabled"); return user; } ``` ### Seamless Dependency Injection Full support for both constructor and method injection: ```csharp public class OrderHandler { // Constructor injection for long-lived dependencies public OrderHandler(ILogger logger) { } // Method injection for per-request dependencies public async Task HandleAsync( CreateOrder command, IOrderRepository repo, // Injected IEmailService email, // Injected CancellationToken ct // Automatically provided ) { var order = await repo.CreateAsync(command); await email.SendOrderConfirmationAsync(order, ct); return order; } } ``` ## Architecture Benefits ### Message-Driven Design Encourages clean, message-oriented architecture: ```csharp // Clear command intent public record CreateOrder(string CustomerId, decimal Amount, string Description); // Explicit query purpose public record GetOrdersByCustomer(string CustomerId, DateTime? Since = null); // Well-defined events public record OrderCreated(string OrderId, string CustomerId, DateTime CreatedAt); ``` ### Automatic Event Publishing Tuple returns enable automatic event cascading which keeps your handlers simple and easy to test: ```csharp public async Task<(Order order, OrderCreated evt)> HandleAsync(CreateOrder cmd) { var order = await _repository.CreateAsync(cmd); // OrderCreated is automatically published to all handlers return (order, new OrderCreated(order.Id, cmd.CustomerId, DateTime.UtcNow)); } ``` ### Cross-Cutting Concerns Made Easy Powerful middleware pipeline for common concerns: ```csharp [FoundatioOrder(10)] public class ValidationMiddleware { public HandlerResult Before(object message) { if (!TryValidate(message, out var errors)) return Result.Invalid(errors); // Short-circuit return HandlerResult.Continue(); } } [FoundatioOrder(20)] public class LoggingMiddleware { public Stopwatch Before(object message) => Stopwatch.StartNew(); public void Finally(object message, Stopwatch sw, Exception? ex) { _logger.LogInformation("Handled {Message} in {Ms}ms", message.GetType().Name, sw.ElapsedMilliseconds); } } ``` ## Testing & Debugging ### Superior Testing Experience Handlers are plain objects - no framework mocking required: ```csharp [Test] public async Task Should_Create_Order_Successfully() { // Arrange var handler = new OrderHandler(_mockLogger.Object); var command = new CreateOrder("CUST-001", 99.99m, "Test order"); // Act var (result, evt) = await handler.HandleAsync(command); // Assert result.IsSuccess.Should().BeTrue(); result.Value.CustomerId.Should().Be("CUST-001"); evt.Should().NotBeNull(); } ``` ### Excellent Debugging Experience Short, simple call stacks make debugging straightforward: ```text Your Code ↓ Generated Interceptor (minimal) ↓ Your Handler Method ``` Compare this to complex reflection-based call stacks in other libraries. ## When to Choose Foundatio Mediator ### ✅ Ideal For * **High-performance applications** where every nanosecond matters * **Clean architecture** implementations with CQRS patterns * **Event-driven systems** with publish/subscribe needs * **Teams preferring conventions** over explicit interfaces * **Applications requiring robust error handling** without exceptions * **Projects wanting excellent testing experience** without framework coupling ### ⚠️ Consider Alternatives When * **Legacy .NET Framework** projects (requires modern .NET) * **Simple CRUD applications** without complex business logic * **Teams preferring explicit interfaces** over conventions * **Existing MediatR codebases** where migration cost isn't justified ## Migration from Other Libraries ### From MediatR Foundatio Mediator provides a migration-friendly approach: ```csharp // MediatR style (still works with some adaptation) public class UserHandler : IRequestHandler { public Task Handle(GetUser request, CancellationToken ct) { } } // Foundatio Mediator style (recommended) public class UserHandler { public Task HandleAsync(GetUser request, CancellationToken ct) { } } ``` ### Migration Benefits * **Better performance** with zero code changes in many cases * **Improved error handling** with Result types * **Enhanced middleware** with state passing * **Reduced boilerplate** with convention-based discovery ## Real-World Success Stories ### Performance-Critical APIs > "We migrated our high-throughput order processing API from MediatR to Foundatio Mediator and saw a 40% reduction in P99 latency while simplifying our error handling patterns." ### Microservices Architecture > "The convention-based approach reduced onboarding time for new team members, and the automatic event publishing simplified our event-driven architecture." ### Large Enterprise Applications > "The compile-time validation caught numerous handler registration issues that would have been runtime errors in our previous mediator library." ## Getting Started Ready to experience the benefits of Foundatio Mediator? 1. [Installation & Setup](./getting-started) - Get running in minutes 2. [Simple Examples](../examples/simple-handlers) - See the patterns in action The combination of exceptional performance, developer-friendly conventions, and robust error handling makes Foundatio Mediator an excellent choice for modern .NET applications.