--- url: /guide/authorization.md --- # Authorization Foundatio.Mediator provides built-in, unified authorization that works for **both** HTTP endpoints and direct `mediator.InvokeAsync()` calls. Authorization requirements are baked into the handler's `HandlerExecutionInfo` at compile time, ensuring zero-reflection enforcement at runtime. ::: tip Events Skip Authorization Authorization only runs on the **invoke** path (`InvokeAsync` / `Invoke`). Handlers triggered via `PublishAsync` or [cascading tuple returns](/guide/cascading-messages) always skip auth checks because events represent something that has already happened — blocking an event handler would leave the system in an inconsistent state. If the event handler itself calls `mediator.InvokeAsync(...)` internally, that nested invoke **will** enforce authorization as normal. ::: ## Quick Start Add `[HandlerAuthorize]` to any handler that requires authentication: ```csharp using Foundatio.Mediator; [HandlerAuthorize] public class SecureHandler { public Task> HandleAsync(GetSecret query, ISecretStore store, CancellationToken ct) { return store.GetAsync(query.Id, ct); } } ``` That's it. The source generator emits an authorization check before the handler runs. If the caller isn't authenticated, Result-returning handlers receive `Result.Unauthorized()` and non-Result handlers throw `UnauthorizedAccessException`. ## How It Works 1. **Compile time** — The source generator reads `[HandlerAuthorize]` and `[HandlerAllowAnonymous]` attributes and assembly-level `AuthorizationRequired`/`AuthorizationPolicies`/`AuthorizationRoles` properties, then bakes the requirements into the generated handler wrapper as an `AuthorizationRequirements` instance on `HandlerExecutionInfo`. 2. **Runtime (invoke path only)** — Before calling the handler method via `InvokeAsync`/`Invoke`, the generated code: * Resolves `IAuthorizationContextProvider` to get the current `ClaimsPrincipal` * Resolves `IHandlerAuthorizationService` to perform the check * Calls `AuthorizeAsync(principal, requirements, cancellationToken)` * Short-circuits with the appropriate unauthorized/forbidden result if the check fails 3. **Zero overhead when not used** — If a handler has no authorization requirements, no authorization code is generated at all. ## Attributes ### `[HandlerAuthorize]` Apply to a handler **class** or **method** to require authorization: ```csharp // Class-level: all methods in this handler require auth [HandlerAuthorize] public class AdminHandler { public Task HandleAsync(DeleteUser command) { ... } public Task> HandleAsync(GetUser query) { ... } } // Method-level: only this specific handler requires auth public class MixedHandler { [HandlerAuthorize] public Task HandleAsync(SensitiveCommand command) { ... } public Task> HandleAsync(PublicQuery query) { ... } } ``` #### Properties | Property | Type | Description | |----------|------|-------------| | `Roles` | `string[]?` | Array of required roles (any-of semantics) | | `Policies` | `string[]?` | Array of authorization policy names to evaluate | ```csharp [HandlerAuthorize(Roles = ["Admin", "Manager"])] public class AdminHandler { ... } [HandlerAuthorize(Policies = ["CanEditProducts", "IsVerified"])] public class ProductHandler { ... } [HandlerAuthorize(Roles = ["Admin"], Policies = ["MfaRequired"])] public class HighSecurityHandler { ... } ``` ### `[HandlerAllowAnonymous]` Apply to a handler class or method to bypass authorization, even when global `AuthorizationRequired = true` is set: ```csharp [HandlerAllowAnonymous] public class PublicHandler { public Task> HandleAsync(HealthCheck query) => ...; } ``` ASP.NET Core's `[AllowAnonymous]` attribute is also recognized and has the same effect. ## Global Configuration Set `AuthorizationRequired = true` on the assembly attribute to require auth for all handlers by default: ```csharp [assembly: MediatorConfiguration( AuthorizationRequired = true, AuthorizationPolicies = ["DefaultPolicy"], AuthorizationRoles = "User" )] ``` Then use `[HandlerAllowAnonymous]` to opt out specific handlers: ```csharp [HandlerAllowAnonymous] public class HealthHandler { public string Handle(HealthCheck query) => "OK"; } ``` ### Precedence Authorization requirements are resolved in this order (most specific wins): 1. **Method-level** `[HandlerAuthorize]` or `[HandlerAllowAnonymous]` 2. **Class-level** `[HandlerAuthorize]` or `[HandlerAllowAnonymous]` 3. **Assembly-level** `AuthorizationRequired` / `AuthorizationPolicies` / `AuthorizationRoles` If a handler has an explicit `[HandlerAuthorize]` or `[HandlerAllowAnonymous]`, the assembly-level defaults are not merged in. ## Authorization Result Handling The behavior when authorization fails depends on the handler's return type: | Return Type | On Unauthorized | On Forbidden | |-------------|-----------------|--------------| | `Result`, `Result` | `Result.Unauthorized("Authentication required.")` | `Result.Forbidden("Access denied.")` | | Other types | `throw UnauthorizedAccessException` | `throw UnauthorizedAccessException` | The distinction between **Unauthorized** (not authenticated) and **Forbidden** (authenticated but lacking permissions) is made by the authorization service based on whether a principal is present. ## Extensibility ### IAuthorizationContextProvider Provides the `ClaimsPrincipal` for the current execution context: ```csharp public interface IAuthorizationContextProvider { ClaimsPrincipal? GetCurrentPrincipal(); } ``` **Auto-registration:** In ASP.NET Core apps (where `IHttpContextAccessor` is available), a provider that reads from `HttpContext.User` is automatically registered. For non-web scenarios, implement and register your own: ```csharp public class WorkerAuthProvider : IAuthorizationContextProvider { public ClaimsPrincipal? GetCurrentPrincipal() { // Return a service identity or read from ambient context return new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.Name, "worker-service") }, "ServiceAuth")); } } // Register in DI services.AddSingleton(); ``` ### IHandlerAuthorizationService Performs the actual authorization check: ```csharp public interface IHandlerAuthorizationService { ValueTask AuthorizeAsync( ClaimsPrincipal? principal, AuthorizationRequirements? requirements, CancellationToken cancellationToken = default); } ``` The default implementation checks: * Whether the principal is authenticated (identity is not null and `IsAuthenticated` is true) * Whether the principal has the required roles (via `IsInRole`) * Whether the principal has claims matching the required policies Replace it to integrate with your own authorization system: ```csharp public class CustomAuthService : IHandlerAuthorizationService { private readonly IAuthorizationService _aspNetAuth; public CustomAuthService(IAuthorizationService aspNetAuth) { _aspNetAuth = aspNetAuth; } public async ValueTask AuthorizeAsync( ClaimsPrincipal? principal, AuthorizationRequirements? requirements, CancellationToken cancellationToken = default) { if (principal?.Identity?.IsAuthenticated != true) return AuthorizationResult.Unauthorized(); if (requirements?.Policies != null) { foreach (var policy in requirements.Policies) { var result = await _aspNetAuth.AuthorizeAsync(principal, policy); if (!result.Succeeded) return AuthorizationResult.Forbidden($"Policy '{policy}' not satisfied."); } } return AuthorizationResult.Success(); } } services.AddSingleton(); ``` ## Events and Publish Authorization is **not enforced** when a handler is triggered through the publish (event) path. This includes: * Direct calls to `mediator.PublishAsync(message)` * Cascading messages from [tuple returns](/guide/cascading-messages) (e.g., `(Result, OrderCreatedEvent)`) * Distributed notifications arriving from other services Events represent facts — something that has already happened. Blocking an event handler with an authorization failure would leave the system in an inconsistent state (the action succeeded but side effects didn't run). Authorization should be enforced at the point where the action is **requested** (the `InvokeAsync` call), not when downstream handlers react to it. If an event handler needs to perform a privileged operation internally, it can call `mediator.InvokeAsync(...)` — that nested invoke will enforce authorization normally. ## Middleware vs Built-in Authorization You can still use middleware for authorization if you prefer: ```csharp [Middleware(Order = 0)] public class AuthorizationMiddleware { public HandlerResult Before(object message, HandlerExecutionInfo info) { if (!IsAuthorized(message, info)) return HandlerResult.ShortCircuit(Result.Unauthorized()); return HandlerResult.Continue(); } } ``` **When to use built-in authorization:** For standard role/policy-based checks that follow a consistent pattern across handlers. **When to use middleware:** For complex, cross-cutting authorization logic that needs access to the full pipeline context, or when you need to authorize based on the message content itself. --- --- 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 Use nullable tuple items to conditionally publish cascaded events. Any `null` cascade item is skipped: ````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 ); } ```csharp public static (Result, OrderCreated?) Handle(CreateOrderCommand command) { if (!IsValid(command)) return (Result.Conflict("Invalid order"), null); // No event published var order = new Order { Email = command.Email }; return (order, new OrderCreated(order.Id, order.Email)); // Event published } ```` ## 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.Error("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, 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, 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 Always declare cascade event types as **nullable** (`?`) in tuple return signatures. This lets you return `null` on error or conditional paths without resorting to `null!`: ```csharp // ✅ Recommended: nullable event types — clean on both success and error paths public (Result, OrderCreated?) Handle(CreateOrder command) { if (!IsValid(command)) return (Result.Invalid("Bad order"), null); // No compiler warning var order = CreateOrder(command); return (order, new OrderCreated(order.Id, order.Email)); // Event published } // ❌ Avoid: non-nullable event types force null! on error paths public (Result, OrderCreated) Handle(CreateOrder command) { if (!IsValid(command)) return (Result.Invalid("Bad order"), null!); // Suppresses warning but defeats null safety // ... } ``` ::: tip The mediator automatically skips publishing any `null` cascade item, so nullable types have zero runtime cost — they only improve the developer experience on error paths. ::: ```` ### 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.Error(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 } } ``` ### Handler Discovery Scope ::: warning Important Event handlers are only discovered in the **current project** and **directly referenced projects**. This is by design for performance - the source generator generates optimized dispatch code at compile time. ::: If your event handlers aren't being called, check the project reference direction: ``` Common.Module (handlers here ARE called when Orders publishes) ↑ Orders.Module (publishes events) ↑ Api (handlers here are NOT called - wrong direction) ``` **Solution:** Place shared event handlers (like audit logging, notifications) in a common module that is referenced by all modules that publish events. See the [Troubleshooting Guide](./troubleshooting.md#event-handlers-not-being-called) for more details. ## Dynamic Subscriptions as an Alternative Cascading messages are handled by **static handlers** discovered at compile time. If you need to consume events **dynamically at runtime** — for example, streaming events to connected browser clients via SSE — use `SubscribeAsync` instead: ```csharp await foreach (var evt in mediator.SubscribeAsync(cancellationToken)) { Console.WriteLine($"Order created: {evt.OrderId}"); } ``` Each subscriber gets its own buffered channel and can filter by concrete type or interface. See [Dynamic Subscriptions](./streaming-handlers#dynamic-subscriptions-with-subscribeasync) for the full API. 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/clean-architecture.md --- # Clean Architecture with Foundatio Mediator Foundatio Mediator is designed to be a natural fit for Clean Architecture applications. Its convention-based approach eliminates boilerplate while enforcing clear boundaries between layers, making it easier to build maintainable, testable, and loosely-coupled systems. ## Why Clean Architecture? ### Preventing the "Big Ball of Mud" Without architectural discipline, applications naturally devolve into a **"Big Ball of Mud"**—a haphazardly structured system where everything depends on everything else. This happens when: * Endpoints directly instantiate repositories and services * Business logic leaks into presentation and database layers * Infrastructure concerns (emails, caching, logging) are scattered throughout * Circular dependencies create impossible-to-test code * Making a change in one place breaks unrelated features **Clean Architecture prevents this chaos through enforced loose coupling.** By organizing code into layers with strict dependency rules, you create natural boundaries that prevent tight coupling from forming. ### Layer Structure and Dependency Rules Clean Architecture organizes code into concentric layers where dependencies point inward—outer layers depend on inner layers, never the reverse: ```text ┌──────────────────────────────────────────────────────────────┐ │ Presentation Layer │ │ (Endpoints, APIs, UI - knows about everything below) │ ├──────────────────────────────────────────────────────────────┤ │ Application Layer │ │ (Handlers, Use Cases - knows about Domain only) │ ├──────────────────────────────────────────────────────────────┤ │ Infrastructure Layer │ │ (Repositories, External Services - implements interfaces) │ ├──────────────────────────────────────────────────────────────┤ │ Domain Layer │ │ (Entities, Value Objects - no external dependencies) │ └──────────────────────────────────────────────────────────────┘ ``` **Key principle:** The Domain layer never depends on Infrastructure. Infrastructure implements interfaces defined by the Domain/Application layers (Dependency Inversion Principle). ### How Loose Coupling Emerges This structure enforces loose coupling at every level: * **Endpoints** don't know about repositories, email services, or databases—they just send messages * **Handlers** depend only on abstractions (interfaces), not concrete implementations * **Domain entities** have no framework dependencies—they're just plain C# objects * **Infrastructure** can be swapped without touching business logic The result: each piece of the system can evolve independently without creating a ripple effect of breaking changes. ### Benefits of This Approach * **Testability** - Business logic can be tested without frameworks or databases * **Maintainability** - Changes in one layer don't cascade to others * **Flexibility** - Infrastructure can be swapped without changing business rules * **Focus** - Each layer has a single responsibility * **Scalability** - Teams can work on different layers without conflicts ## The Mediator Pattern in Clean Architecture The mediator pattern is the perfect complement to Clean Architecture because it **decouples the caller from the handler**. Instead of your presentation layer knowing about services, repositories, and business logic, it simply sends messages. ### Automatic Endpoint Generation With Foundatio Mediator's source generator, you can **eliminate the presentation layer boilerplate entirely**. HTTP endpoints are automatically generated from your handlers: ```csharp [HandlerEndpointGroup("Orders", RoutePrefix = "/api/orders")] public class OrderHandler { /// /// Creates a new order. /// public async Task> HandleAsync( CreateOrder command, IOrderRepository repository, CancellationToken ct) { // Business logic here } /// /// Gets an order by ID. /// public async Task> HandleAsync( GetOrder query, IOrderRepository repository, CancellationToken ct) { return await repository.GetByIdAsync(query.OrderId, ct); } } // Messages define the contract public record CreateOrder(string CustomerId, decimal Amount); public record GetOrder(string OrderId); ``` The source generator automatically creates: * `POST /api/orders` → calls `CreateOrder` handler * `GET /api/orders/{orderId}` → calls `GetOrder` handler **No controller classes. No endpoint registrations. No boilerplate.** Just map them in your startup: ```csharp app.MapMediatorEndpoints(); ``` The generator infers HTTP methods from message names (`Create*` → POST, `Get*` → GET), generates routes from ID properties, maps `Result` to HTTP status codes, and pulls OpenAPI metadata from XML doc comments. ### Manual Endpoints (When Needed) If you prefer explicit control or need custom endpoint behavior, you can still write manual endpoints: ```csharp // MVC Controller [HttpPost] public async Task CreateOrder(CreateOrderRequest request) { var result = await _mediator.InvokeAsync>( new CreateOrder(request.CustomerId, request.Amount)); return result.ToActionResult(); } // Minimal API app.MapPost("/orders", async (CreateOrderRequest request, IMediator mediator) => { var result = await mediator.InvokeAsync>( new CreateOrder(request.CustomerId, request.Amount)); return result.ToActionResult(); }); ``` Either way, your presentation layer stays thin and focused on HTTP concerns while business logic lives entirely in handlers. ## How Foundatio Mediator Enables Clean Architecture ### 1. Low-Ceremony Handler Definition Unlike traditional mediator implementations that require interface inheritance and rigid method signatures, Foundatio Mediator uses conventions: ```csharp // Traditional mediator libraries - lots of ceremony public class CreateOrderHandler : IRequestHandler> { private readonly IOrderRepository _repository; public CreateOrderHandler(IOrderRepository repository) { _repository = repository; } public async Task> Handle(CreateOrder request, CancellationToken ct) { // Business logic } } // Foundatio Mediator - just follow naming conventions public class OrderHandler { public async Task> HandleAsync( CreateOrder command, IOrderRepository repository, // Method injection - no constructor needed CancellationToken ct) { // Business logic } } ``` **Benefits:** * No interface inheritance required * Method injection means less boilerplate * Multiple handlers per class for related operations * Sync or async—you choose based on your needs ### 2. Natural Command/Query Separation (CQRS) Clean Architecture naturally leads to CQRS because queries and commands have different characteristics. Foundatio Mediator makes this separation effortless: ```csharp // Commands - change state, return results public record CreateOrder(string CustomerId, decimal Amount); public record UpdateOrderStatus(string OrderId, OrderStatus Status); public record CancelOrder(string OrderId, string Reason); // Queries - read state, never modify public record GetOrder(string OrderId); public record GetOrdersByCustomer(string CustomerId, DateTime? Since = null); public record GetDashboardReport(); // Handler can group related operations naturally public class OrderHandler { // Commands public async Task> HandleAsync(CreateOrder cmd, IOrderRepository repo, CancellationToken ct) => await repo.CreateAsync(cmd, ct); public async Task> HandleAsync(UpdateOrderStatus cmd, IOrderRepository repo, CancellationToken ct) => await repo.UpdateStatusAsync(cmd.OrderId, cmd.Status, ct); // Queries public async Task> HandleAsync(GetOrder query, IOrderRepository repo, CancellationToken ct) => await repo.GetByIdAsync(query.OrderId, ct); public async Task>> HandleAsync(GetOrdersByCustomer query, IOrderRepository repo, CancellationToken ct) => await repo.GetByCustomerAsync(query.CustomerId, query.Since, ct); } ``` ### 3. Domain Events for Loose Coupling When a business operation completes, other parts of the system often need to react—send emails, update analytics, log audits. Traditional approaches create tight coupling: ```csharp // Tight coupling - handler knows about all side effects public async Task HandleAsync(CreateOrder cmd) { var order = await _repository.CreateAsync(cmd); await _emailService.SendOrderConfirmationAsync(order); // Coupling await _analyticsService.TrackOrderAsync(order); // Coupling await _auditService.LogOrderCreatedAsync(order); // Coupling return order; } ``` With Foundatio Mediator's cascading messages, handlers publish events and don't care who handles them: ```csharp // Loose coupling - handler just publishes an event public async Task<(Result, OrderCreated)> HandleAsync( CreateOrder cmd, IOrderRepository repository, CancellationToken ct) { var order = await repository.CreateAsync(cmd, ct); // Return the result AND an event - mediator publishes it automatically return (order, new OrderCreated(order.Id, order.CustomerId, DateTime.UtcNow)); } // Event handlers are completely decoupled - add/remove without touching OrderHandler public class EmailHandler { public async Task HandleAsync(OrderCreated evt, IEmailService email, CancellationToken ct) => await email.SendOrderConfirmationAsync(evt.OrderId, ct); } public class AnalyticsHandler { public async Task HandleAsync(OrderCreated evt, IAnalyticsService analytics, CancellationToken ct) => await analytics.TrackOrderAsync(evt.OrderId, evt.CustomerId, ct); } public class AuditHandler { public async Task HandleAsync(OrderCreated evt, IAuditService audit, CancellationToken ct) => await audit.LogAsync("OrderCreated", evt.OrderId, ct); } ``` **Benefits:** * Adding new reactions requires zero changes to the publishing handler * Event handlers can live in different modules/assemblies * Easy to test each handler in isolation * Clear audit trail of system behavior ### 4. Modular Monolith Support Clean Architecture shines in modular monoliths where bounded contexts are separated into modules. Foundatio Mediator enables cross-module communication without creating dependencies: ```text ┌──────────────────────────────────────────────────────────────┐ │ Common.Module │ │ Events, Middleware, Shared Services │ ├──────────────────────┬───────────────────────────────────────┤ │ Orders.Module │ Products.Module │ │ OrderHandler │ ProductHandler │ │ Order Domain │ Product Domain │ ├──────────────────────┴───────────────────────────────────────┤ │ Reports.Module │ │ Queries data from Orders and Products via mediator │ └──────────────────────────────────────────────────────────────┘ ``` ```csharp // Reports.Module doesn't reference Orders or Products directly // It queries through the mediator public class ReportHandler { public async Task HandleAsync( GetDashboardReport query, IMediator mediator, CancellationToken ct) { // Fetch from other modules via mediator - no direct dependencies var ordersTask = mediator.InvokeAsync>(new GetOrders(), ct); var productsTask = mediator.InvokeAsync>(new GetProducts(), ct); await Task.WhenAll(ordersTask.AsTask(), productsTask.AsTask()); return new DashboardReport( TotalOrders: ordersTask.Result.Count, TotalProducts: productsTask.Result.Count, Revenue: ordersTask.Result.Sum(o => o.Amount) ); } } ``` ### 5. Cross-Cutting Concerns via Middleware Clean Architecture requires cross-cutting concerns (logging, validation, caching) to be handled consistently without polluting business logic. Foundatio Mediator's middleware pipeline makes this natural: ```csharp // Validation middleware - runs before every handler public class ValidationMiddleware { public HandlerResult Before(object message) { if (!MiniValidator.TryValidate(message, out var errors)) return HandlerResult.ShortCircuit(Result.Invalid(errors)); return HandlerResult.Continue(); } } // Logging middleware - tracks all handler execution public class ObservabilityMiddleware { public Stopwatch Before(object message, HandlerExecutionInfo info, ILogger logger) { logger.LogInformation("Handling {Handler}", info.HandlerType.Name); return Stopwatch.StartNew(); } public void After(object message, Stopwatch sw, HandlerExecutionInfo info, ILogger logger) { logger.LogInformation("Completed {Handler} in {Ms}ms", info.HandlerType.Name, sw.ElapsedMilliseconds); } public void Finally(object message, Stopwatch sw, Exception? ex, ILogger logger) { if (ex != null) logger.LogError(ex, "Handler failed after {Ms}ms", sw.ElapsedMilliseconds); } } ``` Middleware is automatically applied to all handlers—no manual registration or decorator patterns needed. ### 6. Zero-Boilerplate HTTP Endpoints Traditional Clean Architecture implementations still require significant presentation layer code—controllers, endpoint registrations, parameter binding, and response mapping. Foundatio Mediator's source generator eliminates this entirely: ```csharp // Traditional approach - lots of presentation layer code public class OrdersController : ControllerBase { private readonly IMediator _mediator; public OrdersController(IMediator mediator) => _mediator = mediator; [HttpPost] public async Task Create([FromBody] CreateOrderRequest request) { var result = await _mediator.InvokeAsync>( new CreateOrder(request.CustomerId, request.Amount)); return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Errors); } [HttpGet("{id}")] public async Task Get(string id) { var result = await _mediator.InvokeAsync>(new GetOrder(id)); return result.IsSuccess ? Ok(result.Value) : NotFound(); } // ... repeat for every operation } // Foundatio Mediator - handlers ARE the API [HandlerEndpointGroup("Orders", RoutePrefix = "/api/orders")] public class OrderHandler { public async Task> HandleAsync(CreateOrder cmd, IOrderRepository repo, CancellationToken ct) => await repo.CreateAsync(cmd, ct); public async Task> HandleAsync(GetOrder query, IOrderRepository repo, CancellationToken ct) => await repo.GetByIdAsync(query.OrderId, ct); } ``` **Benefits:** * **No controllers or endpoint classes** - handlers define the API surface * **Automatic HTTP method inference** - `Create*` → POST, `Get*` → GET, `Update*` → PUT, `Delete*` → DELETE * **Automatic route generation** - ID properties become route parameters * **Result-to-HTTP mapping** - `Result.NotFound()` → 404, `Result.Invalid()` → 400, etc. * **OpenAPI generation** - XML doc comments become endpoint summaries * **Authentication built-in** - Configure auth at group or endpoint level See [Automatic Endpoint Generation](./endpoints) for full documentation. ## Project Structure Example Here's a recommended structure for a Clean Architecture application using Foundatio Mediator: ```text src/ ├── Domain/ # Pure domain - no dependencies │ ├── Entities/ │ │ ├── Order.cs │ │ └── Product.cs │ └── ValueObjects/ │ └── Money.cs │ ├── Application/ # Handlers and business logic │ ├── Orders/ │ │ ├── Commands/ │ │ │ ├── CreateOrder.cs │ │ │ └── CancelOrder.cs │ │ ├── Queries/ │ │ │ ├── GetOrder.cs │ │ │ └── GetOrdersByCustomer.cs │ │ ├── Events/ │ │ │ └── OrderCreated.cs │ │ └── OrderHandler.cs │ ├── Products/ │ │ └── ... │ └── Common/ │ ├── Middleware/ │ │ ├── ValidationMiddleware.cs │ │ └── LoggingMiddleware.cs │ └── Interfaces/ │ ├── IOrderRepository.cs │ └── IProductRepository.cs │ ├── Infrastructure/ # External concerns │ ├── Persistence/ │ │ ├── OrderRepository.cs │ │ └── ProductRepository.cs │ └── Services/ │ └── EmailService.cs │ └── Web/ # Presentation layer └── Program.cs # Endpoints auto-generated from handlers ``` ## Real-World Example See the [CleanArchitectureSample](https://github.com/FoundatioFx/Foundatio.Mediator/tree/main/samples/CleanArchitectureSample) in the repository for a complete working example demonstrating: * Multiple bounded contexts (Orders, Products, Reports) * Cross-module communication via mediator * Domain events with cascading messages * Shared middleware for validation and observability * Repository pattern with in-memory implementations ## Key Benefits Summary | Traditional Approach | With Foundatio Mediator | |---------------------|------------------------| | Endpoints know about services, repositories, business logic | Endpoints only know about messages and mediator | | Tight coupling between modules | Loose coupling via messages and events | | Interface boilerplate for every handler | Convention-based discovery, zero interfaces | | Manual event publishing and subscription | Automatic cascading with tuple returns | | Cross-cutting concerns scattered or complex decorators | Simple middleware with Before/After/Finally/Execute | | One handler per class limitation | Multiple handlers per class, grouped naturally | | Controllers/endpoints for every operation | Auto-generated endpoints from handlers | | Manual HTTP status code mapping | Result types map to HTTP status automatically | ## Next Steps * [Getting Started](./getting-started) - Set up Foundatio Mediator * [Automatic Endpoint Generation](./endpoints) - Zero-boilerplate HTTP APIs * [Cascading Messages](./cascading-messages) - Domain events and event-driven architecture * [Middleware](./middleware) - Cross-cutting concerns * [CleanArchitectureSample](https://github.com/FoundatioFx/Foundatio.Mediator/tree/main/samples/CleanArchitectureSample) - Complete working example --- --- url: /guide/configuration.md --- # Configuration Options ::: tip You Probably Don't Need This Foundatio Mediator works out of the box with sensible defaults — most projects never need to configure anything beyond `services.AddMediator()`. Only reach for the options below when you want to change a specific default behavior. ::: Foundatio Mediator provides two types of configuration: **compile-time configuration** via the `[assembly: MediatorConfiguration]` attribute that controls source generator behavior, and **runtime configuration** via the `AddMediator()` method that controls mediator behavior. ## Compile-Time Configuration (Assembly Attribute) All source generator settings—handler discovery, lifetimes, interceptors, telemetry, and endpoint generation—are configured through a single assembly-level attribute: ```csharp using Foundatio.Mediator; [assembly: MediatorConfiguration( HandlerLifetime = MediatorLifetime.Scoped, EndpointDiscovery = EndpointDiscovery.All, EndpointRoutePrefix = "api" )] ``` ### Property Details **`HandlerLifetime`** (`MediatorLifetime` enum) * **Values:** `Default`, `Transient`, `Scoped`, `Singleton` * **Default:** `Default` (handlers use internal caching) * **Effect:** Registers all discovered handlers with the specified DI lifetime, unless overridden by `[Handler(Lifetime = ...)]` attribute * **Behavior by value:** * `Scoped`/`Transient`/`Singleton`: Always resolved from DI on every invocation * `Default`: Handlers are cached internally (no constructor deps → `new()`, with constructor deps → `ActivatorUtilities.CreateInstance`) **`MiddlewareLifetime`** (`MediatorLifetime` enum) * **Values:** `Default`, `Transient`, `Scoped`, `Singleton` * **Default:** `Default` (middleware uses internal caching) * **Effect:** Registers all discovered middleware with the specified DI lifetime, unless overridden by `[Middleware(Lifetime = ...)]` attribute * **Behavior by value:** * `Scoped`/`Transient`/`Singleton`: Always resolved from DI on every invocation * `Default`: Middleware is cached internally (no constructor deps → `new()`, with constructor deps → `ActivatorUtilities.CreateInstance`) ### Per-Handler Lifetime Override Individual handlers can override the project-level default lifetime using the `[Handler]` attribute: ```csharp // Uses project-level HandlerLifetime from [assembly: MediatorConfiguration] public class DefaultLifetimeHandler { public Task HandleAsync(MyMessage msg) => Task.CompletedTask; } // Explicitly registered as Singleton (overrides project default) [Handler(Lifetime = MediatorLifetime.Singleton)] public class CachedDataHandler { public CachedData Handle(GetCachedData query) => _cache.Get(); } // Explicitly registered as Transient [Handler(Lifetime = MediatorLifetime.Transient)] public class StatelessHandler { public void Handle(FireAndForgetEvent evt) { } } // Combined with Order for publish scenarios [Handler(Order = 1, Lifetime = MediatorLifetime.Scoped)] public class FirstScopedHandler { public void Handle(MyEvent evt) { } } ``` **Available `MediatorLifetime` values:** * `MediatorLifetime.Default` - Use project-level `HandlerLifetime` from `[assembly: MediatorConfiguration]` * `MediatorLifetime.Transient` - New instance per request * `MediatorLifetime.Scoped` - Same instance within a scope * `MediatorLifetime.Singleton` - Single instance for application lifetime ### Per-Middleware Lifetime Override Individual middleware can override the project-level default lifetime using the `[Middleware]` attribute: ```csharp // Uses project-level MiddlewareLifetime from [assembly: MediatorConfiguration] public class DefaultLifetimeMiddleware { public void Before(object msg) { } } // Explicitly registered as Singleton (overrides project default) [Middleware(Lifetime = MediatorLifetime.Singleton)] public class CachingMiddleware { private readonly IMemoryCache _cache; public CachingMiddleware(IMemoryCache cache) => _cache = cache; public void Before(object msg) { /* caching logic */ } } // Explicitly registered as Scoped [Middleware(Lifetime = MediatorLifetime.Scoped)] public class RequestScopedMiddleware { private readonly IHttpContextAccessor _httpContext; public RequestScopedMiddleware(IHttpContextAccessor httpContext) => _httpContext = httpContext; public void Before(object msg) { /* request-scoped logic */ } } // Combined with Order for execution ordering [Middleware(Order = 1, Lifetime = MediatorLifetime.Transient)] public class FirstTransientMiddleware { public void Before(object msg) { } } ``` **`DisableInterceptors`** (`bool`) * **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 **`DisableOpenTelemetry`** (`bool`) * **Default:** `false` * **Effect:** When `true`, disables OpenTelemetry integration code generation * **Use Case:** Reduce generated code size when telemetry is not needed **`DisableAuthorization`** (`bool`) * **Default:** `false` * **Effect:** When `true`, disables all generated authorization checks in handler code and prevents registration of authorization-related services (`IHttpContextAccessor`, `HttpContextAuthorizationContextProvider`, `IAuthorizationContextProvider`). `[HandlerAuthorize]` attributes are ignored for inline mediator call auth checks. Endpoint-level `.RequireAuthorization()` is **not** affected. * **Use Case:** Projects that don't need mediator-level authorization, or projects that want to avoid `IHttpContextAccessor` being automatically registered **`HandlerDiscovery`** (`HandlerDiscovery` enum) * **Values:** `All`, `Explicit` * **Default:** `All` * **Effect:** Controls how handlers are discovered at compile time * `All`: Convention-based discovery (class names ending with `Handler` or `Consumer`) plus `IHandler` interface and `[Handler]` attribute * `Explicit`: Only handlers that implement `IHandler` interface or have the `[Handler]` attribute will be discovered * **Use Case:** Explicit control over which classes are treated as handlers, avoiding accidental handler discovery **`NotificationPublishStrategy`** (`NotificationPublishStrategy` enum) * **Values:** `ForeachAwait`, `TaskWhenAll`, `FireAndForget` * **Default:** `ForeachAwait` * **Effect:** Controls how `PublishAsync` dispatches messages to multiple handlers * `ForeachAwait`: Invokes handlers sequentially, one at a time * `TaskWhenAll`: Invokes all handlers concurrently and waits for all to complete * `FireAndForget`: Fires all handlers in parallel without waiting **`EnableGenerationCounter`** (`bool`) * **Default:** `false` * **Effect:** When `true`, includes a generation counter comment in generated files * **Use Case:** Debugging source generator incremental caching ### Endpoint Properties The following properties on `MediatorConfigurationAttribute` control endpoint generation: **`EndpointDiscovery`** (`EndpointDiscovery` enum) * **Values:** `None`, `Explicit`, `All` * **Default:** `All` * **Effect:** Controls which handlers generate API endpoints * `All`: All handlers with endpoint-compatible message types generate endpoints (default). Use `[HandlerEndpoint(Exclude = true)]` to opt out individual handlers. * `Explicit`: Only handlers with `[HandlerEndpoint]` or `[HandlerEndpointGroup]` attribute generate endpoints * `None`: No endpoints generated * **See:** [Endpoints Guide](/guide/endpoints) for full documentation **`EndpointRoutePrefix`** (`string?`) * **Default:** `"api"` * **Effect:** Sets a global route prefix that all endpoint groups nest under. Groups auto-derive their route from their name (e.g., `[HandlerEndpointGroup("Products")]` → `products`), composing with the global prefix to produce `/api/products`. Convention-based entity routes are auto-pluralized (e.g., `GetProduct` → `/products/{productId}`). * **Important:** Group-level `RoutePrefix` values without a leading `/` are **relative** to this global prefix. Don't include `api` in your group prefixes when using the default global prefix, or you'll get `/api/api/...`. Use a leading `/` on a group prefix to make it absolute (bypasses the global prefix). * **To disable:** Set `EndpointRoutePrefix = ""` to remove the global prefix entirely, then use full paths in group prefixes. **`AuthorizationRequired`** (`bool`) * **Default:** `false` * **Effect:** Sets the default authorization requirement for all handlers (both endpoints and direct mediator calls) * **Use Case:** Secure-by-default API with opt-out for public handlers * **Override:** Use `[HandlerAllowAnonymous]` on a handler class or method to opt out, or `[HandlerAuthorize]` to opt in specific handlers when this is `false` **`EndpointFilters`** (`Type[]?`) * **Default:** None * **Effect:** Applies endpoint filters to the root MapGroup, affecting all generated endpoints * **Example:** `EndpointFilters = new[] { typeof(LoggingFilter), typeof(ValidationFilter) }` **`AuthorizationPolicies`** / **`AuthorizationRoles`** * **Values:** String array / String array * **Default:** None * **Effect:** Sets default authorization policies and roles for all handlers globally **`EndpointSummaryStyle`** (`EndpointSummaryStyle` enum) * **Values:** `Exact`, `Spaced` * **Default:** `Exact` * **Effect:** Controls how endpoint summaries are generated from message type names * `Exact`: Uses the message type name as-is (e.g., `"GetProduct"`) * `Spaced`: Splits PascalCase into space-separated words (e.g., `"Get Product"`) ### Example Configuration All configuration is done via the assembly attribute in any `.cs` file in your project: ```csharp using Foundatio.Mediator; [assembly: MediatorConfiguration( HandlerLifetime = MediatorLifetime.Scoped, MiddlewareLifetime = MediatorLifetime.Scoped, EndpointDiscovery = EndpointDiscovery.All, EndpointRoutePrefix = "api" )] ``` Your `.csproj` only needs the package reference and optional XML doc generation: ```xml net10.0 true $(NoWarn);CS1591 ``` ## 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 ### MediatorOptions Class ```csharp public class MediatorOptions { public List Assemblies { get; set; } = []; public ServiceLifetime? MediatorLifetime { get; set; } // null = auto-detect public bool LogHandlers { get; set; } // Log all discovered handlers at startup public bool LogMiddleware { get; set; } // Log the middleware pipeline at startup } ``` When `MediatorLifetime` is `null` (the default), the mediator is registered as **Scoped** in ASP.NET Core apps and **Singleton** otherwise. Set it explicitly to override auto-detection. When `LogHandlers` is `true`, all registered handlers are printed in a formatted, aligned table during `AddMediator()`. When `LogMiddleware` is `true`, the middleware pipeline is printed in execution order: ```csharp services.AddMediator(new MediatorOptions { LogHandlers = true, LogMiddleware = true }); // or services.AddMediator(b => b.LogHandlers().LogMiddleware()); ``` ### Notification Publishers Foundatio Mediator provides three built-in notification publishers that control how `PublishAsync` dispatches messages to multiple handlers: | Publisher | Behavior | Use Case | |-----------|----------|----------| | `ForeachAwaitPublisher` | Invokes handlers **sequentially**, one at a time (default) | Predictable ordering, easier debugging | | `TaskWhenAllPublisher` | Invokes all handlers **concurrently** and waits for all to complete | Maximum throughput when handlers are independent | | `FireAndForgetPublisher` | Fires all handlers **in parallel without waiting** | Background events where you don't need to wait for completion | **Example:** ```csharp // Use parallel execution with await builder.Services.AddMediator(cfg => cfg .UseNotificationPublisher(new TaskWhenAllPublisher())); // Fire and forget - returns immediately builder.Services.AddMediator(cfg => cfg .UseNotificationPublisher(new FireAndForgetPublisher())); ``` > ⚠️ **Warning:** `FireAndForgetPublisher` swallows exceptions and handlers may outlive the HTTP request. Use with caution and ensure proper error handling within your handlers. ## 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(); ``` ## Assembly-Level Configuration Disable interceptors if you need to force DI dispatch: ```csharp [assembly: MediatorConfiguration(DisableInterceptors = 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(); ``` ## Mediator Lifetime and Scoped Services The mediator lifetime is **auto-detected** by default: * **ASP.NET Core apps** → registered as **Scoped** (one instance per HTTP request) * **Console / worker apps** → registered as **Singleton** This means `services.AddMediator()` does the right thing automatically — scoped services like `DbContext` are resolved from the correct per-request scope in web apps without any extra configuration. ### Overriding the Default You can explicitly set the lifetime if needed: ```csharp // Force singleton (e.g., console app where all services are singleton) services.AddMediator(b => b.SetMediatorLifetime(ServiceLifetime.Singleton)); // Force scoped (e.g., worker service with scoped DbContext) services.AddMediator(b => b.SetMediatorLifetime(ServiceLifetime.Scoped)); ``` ### When to Override | Scenario | Override needed? | | -------- | --------------------- | | ASP.NET Core with `DbContext` or scoped services | No (auto-detected as Scoped) | | Console app with only singletons | No (auto-detected as Singleton) | | Worker service with scoped services | **Yes** — use `SetMediatorLifetime(ServiceLifetime.Scoped)` | | Console app that needs Scoped | **Yes** — use `SetMediatorLifetime(ServiceLifetime.Scoped)` | ## Middleware Lifetime Middleware lifetime follows the same rules as handler lifetime: | Lifetime | Behavior | |----------|----------| | **Scoped** | Resolved from DI on every invocation | | **Transient** | Resolved from DI on every invocation | | **Singleton** | Resolved from DI on every invocation (DI handles caching) | | **None/Default** (no constructor deps) | Created once with `new()` and cached in static field | | **None/Default** (with constructor deps) | Created once with `ActivatorUtilities.CreateInstance` and cached | ## Handler Lifetime Management ### Lifetime Behavior Summary | Lifetime | Behavior | |----------|----------| | **Scoped** | Resolved from DI on every invocation | | **Transient** | Resolved from DI on every invocation | | **Singleton** | Resolved from DI on every invocation (DI handles caching) | | **None/Default** (no constructor deps) | Created once with `new()` and cached in static field | | **None/Default** (with constructor deps) | Created once with `ActivatorUtilities.CreateInstance` and cached | ### Important: Default Behavior When Lifetime Not Specified If you don't explicitly set a lifetime (via `[Handler(Lifetime = ...)]` or `HandlerLifetime` in `[assembly: MediatorConfiguration]`), the handler instance will be cached: * **No constructor parameters**: Instantiated with `new()` and cached forever * **With constructor parameters**: Created via `ActivatorUtilities.CreateInstance` and cached - constructor dependencies are resolved once and reused ```csharp // WARNING: This handler is cached - 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()); } } ``` ### Explicit Lifetime Always Uses DI When you explicitly set a lifetime (`Scoped`, `Transient`, or `Singleton`), the handler is **always resolved from the DI container**: ```csharp // Singleton - resolved from DI, DI handles the singleton caching [Handler(Lifetime = MediatorLifetime.Singleton)] public class CacheHandler { } // Scoped - resolved from DI on each invocation [Handler(Lifetime = MediatorLifetime.Scoped)] public class OrderHandler { } ``` This ensures proper test isolation - each test with its own DI container gets its own handler instances. ### Controlling Lifetime There are two ways to control handler lifetime: **1. Using the `[Handler]` attribute:** ```csharp [Handler(Lifetime = MediatorLifetime.Scoped)] public class OrderHandler { /* ... */ } ``` **2. Using the `HandlerLifetime` property on `MediatorConfiguration`** (see below) ### Automatic Handler Registration with Assembly Attribute You can automatically register all handlers in your project with a specific lifetime using the `HandlerLifetime` property on `[assembly: MediatorConfiguration]`: ```csharp using Foundatio.Mediator; [assembly: MediatorConfiguration(HandlerLifetime = MediatorLifetime.Scoped)] ``` **Supported Values:** * `MediatorLifetime.Scoped` - Handlers registered as scoped services * `MediatorLifetime.Transient` - Handlers registered as transient services * `MediatorLifetime.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:** ```csharp using Foundatio.Mediator; // In any .cs file in your project [assembly: MediatorConfiguration(HandlerLifetime = MediatorLifetime.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(); ``` ### Per-Handler Lifetime Override Individual handlers can override the project-level default lifetime using the `[Handler]` attribute: ```csharp // Uses project-level HandlerLifetime from [assembly: MediatorConfiguration] public class DefaultHandler { public Task HandleAsync(MyMessage msg) => Task.CompletedTask; } // Explicitly registered as Singleton (overrides project default) [Handler(Lifetime = MediatorLifetime.Singleton)] public class CacheHandler { private readonly InMemoryCache _cache = new(); public CachedData Handle(GetCachedData query) => _cache.Get(query.Key); } // Explicitly registered as Transient [Handler(Lifetime = MediatorLifetime.Transient)] public class StatelessHandler { public void Handle(LogEvent evt) { /* ... */ } } // Explicitly registered as Scoped (even if project default is different) [Handler(Lifetime = MediatorLifetime.Scoped)] public class ScopedHandler { private readonly DbContext _db; public ScopedHandler(DbContext db) => _db = db; public async Task HandleAsync(GetOrder query) { return await _db.Orders.FindAsync(query.Id); } } ``` **Available `MediatorLifetime` values:** * `MediatorLifetime.Default` - Use project-level `HandlerLifetime` from `[assembly: MediatorConfiguration]` * `MediatorLifetime.Transient` - New instance per request * `MediatorLifetime.Scoped` - Same instance within a scope * `MediatorLifetime.Singleton` - Single instance for application lifetime ## 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 * `HttpContext`, `HttpRequest`, `HttpResponse` - Automatically available when called from a [generated endpoint](./endpoints#accessing-http-types-in-handlers) * Repository interfaces * Business service interfaces * Configuration objects ### Default Behavior (No Explicit Lifetime) When no lifetime is specified, middleware instances are cached: ```csharp // No explicit lifetime - cached with new() since no constructor deps public class SimpleMiddleware { public void Before(object message) { /* ... */ } } // No explicit lifetime - cached via ActivatorUtilities since it has constructor deps public class LoggingMiddleware { private readonly ILogger _logger; public LoggingMiddleware(ILogger logger) { _logger = logger; // Resolved once and cached! } public void Before(object message) { _logger.LogInformation("Handling {MessageType}", message.GetType().Name); } } ``` ### Explicit Lifetime with \[Middleware] Attribute Use the `[Middleware]` attribute to explicitly control lifetime: ```csharp // Resolved from DI on every invocation - DI handles singleton caching [Middleware(Lifetime = MediatorLifetime.Singleton)] public class LoggingMiddleware { /* ... */ } // Resolved from DI on every invocation [Middleware(Lifetime = MediatorLifetime.Scoped)] public class ValidationMiddleware { /* ... */ } ``` ### Project-Level Default with Assembly Attribute Set a default lifetime for all middleware using `MiddlewareLifetime` in `[assembly: MediatorConfiguration]`: ```csharp [assembly: MediatorConfiguration(MiddlewareLifetime = MediatorLifetime.Scoped)] ``` ## 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 // uses scoped Mediator lifetime to be compatible with scoped/transient services builder.Services.AddMediator(b => b.SetMediatorLifetime(ServiceLifetime.Scoped)); 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: /guide/endpoints.md --- # Endpoint Generation ::: tip Optional Feature Endpoint generation is entirely **opt-in**. Foundatio Mediator works perfectly as a pure in-process mediator without any HTTP layer. If you don't call `.MapMediatorEndpoints()`, no endpoints are generated and no ASP.NET Core dependencies are required. Use this feature only when you want to expose your handlers as a REST API. ::: Foundatio Mediator automatically generates ASP.NET Core Minimal API endpoints from your handlers. Write your handlers as plain message-in/result-out methods, call `.MapMediatorEndpoints()`, and you have a fully functional API — with smart route conventions, HTTP method inference, and OpenAPI metadata — all without writing a single controller or endpoint definition. Because your handler logic is completely decoupled from HTTP, it's trivially testable: just call the handler directly and assert the result. The endpoint layer is a thin, generated projection that you never maintain by hand. ## Quick Start Write a handler: ```csharp public class ProductHandler { /// Create a new product public Task> HandleAsync(CreateProduct command) { ... } /// Get a product by ID public Result Handle(GetProduct query) { ... } /// List all products public Result> Handle(GetProducts query) { ... } public Task> HandleAsync(UpdateProduct command) { ... } public Task HandleAsync(DeleteProduct command) { ... } } ``` Map it in your startup: ```csharp var app = builder.Build(); app.MapMediatorEndpoints(); app.Run(); ``` That's it. You now have: ```text POST /api/products → CreateProduct GET /api/products/{productId} → GetProduct GET /api/products → GetProducts PUT /api/products/{productId} → UpdateProduct DELETE /api/products/{productId} → DeleteProduct ``` No attributes required. The source generator infers everything from your message names and properties: * **HTTP method** — from the message name prefix (`Get*` → GET, `Create*` → POST, `Update*` → PUT, `Delete*` → DELETE, etc.) * **Route** — from the message name (minus the verb prefix), **auto-pluralized** to follow REST conventions, with message properties (names ending in `Id` become route parameters) * **Parameter binding** — ID properties go in the route, other properties become query parameters (GET/DELETE) or body (POST/PUT/PATCH) * **OpenAPI metadata** — operation names, status codes, and even error responses are auto-detected from your `Result` factory calls. XML `` comments on handler methods automatically become the OpenAPI summary for each endpoint. * **Result mapping** — `Result` return values are automatically converted to the correct HTTP status codes ### Why This Matters This architecture gives you a **loosely coupled, message-oriented application** with close to zero boilerplate. Your handlers don't know they're behind HTTP — they receive a message and return a result. This means: * **Testing is trivial** — handlers are plain methods with no framework code, so you can call them directly in a unit test. No mediator, no `HttpClient`, no test server, no request serialization. * **Transport-agnostic** — the same handler works through HTTP endpoints, direct mediator calls, background jobs, or SignalR — the handler doesn't care. * **Always in sync** — endpoints are generated from your handler code, so your API can never drift from your business logic. ## Streaming Endpoints & Server-Sent Events Handlers that return `IAsyncEnumerable` automatically become streaming HTTP endpoints. Combined with `SubscribeAsync`, you can push real-time domain events to the browser in just a few lines: ```csharp public record GetEventStream; public record ClientEvent(string EventType, object Data); public class EventStreamHandler(IMediator mediator) { [HandlerEndpoint(Streaming = EndpointStreaming.ServerSentEvents)] public async IAsyncEnumerable Handle( GetEventStream message, [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var evt in mediator.SubscribeAsync( cancellationToken)) { yield return new ClientEvent(evt.GetType().Name, evt); } } } ``` That generates `GET /api/events` as an SSE endpoint. Any browser client can subscribe: ```javascript const source = new EventSource('/api/events'); source.onmessage = (e) => { const event = JSON.parse(e.data); console.log(event.eventType, event.data); }; ``` Whenever any handler publishes a notification, every connected SSE client receives it instantly. Zero polling, zero WebSocket infrastructure. ### JSON Array Streaming Without the SSE attribute, streaming handlers return a JSON array streamed incrementally — useful for large datasets that you don't want to buffer in memory: ```csharp public class SalesHandler { public async IAsyncEnumerable HandleAsync( GetSalesStream query, ISalesRepository repository, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var record in repository.GetSalesAsync(query.Year, cancellationToken)) { yield return record; } } } ``` This generates a `GET /api/sales` endpoint that streams results as they're produced — ASP.NET Core sends each item to the client without waiting for the full result set. | `Streaming` Value | Behavior | | --- | --- | | `EndpointStreaming.Default` | JSON array streaming (default for `IAsyncEnumerable` handlers) | | `EndpointStreaming.ServerSentEvents` | SSE via `TypedResults.ServerSentEvents()` (.NET 10+) | | `SseEventType` | Behavior | | --- | --- | | `null` (default) | Browser `EventSource` fires the default `message` event | | `"event"` | Clients listen with `addEventListener('event', ...)` | ::: tip For `SubscribeAsync`, dynamic subscriptions, and SSE details, see [Streaming Handlers](./streaming-handlers.md). ::: ## Customization Attributes Everything works out of the box with smart defaults. Attributes are only needed when you want to change a default behavior — group endpoints with a shared route prefix, override a route, change an HTTP method, or exclude a handler from generation. ### `[HandlerEndpointGroup]` — Group Endpoints `[HandlerEndpointGroup]` is applied to a handler **class** and controls all endpoints in that class as a group. Use it to set a shared route prefix, OpenAPI tag, or endpoint filters for every handler method on the class. ```csharp [HandlerEndpointGroup(RoutePrefix = "v2/products")] public class ProductHandler { public Task> HandleAsync(CreateProduct command) { ... } public Result Handle(GetProduct query) { ... } } ``` This overrides the default route prefix (which would be `products` from the class name) with a versioned path: ```text POST /api/v2/products → CreateProduct GET /api/v2/products/{productId} → GetProduct ``` **When do you need `[HandlerEndpointGroup]`?** * To set a **custom route prefix** different from the class name: `[HandlerEndpointGroup(RoutePrefix = "v2/products")]` * To set a **custom OpenAPI tag** different from the class name: `[HandlerEndpointGroup(Name = "Inventory")]` * To apply **endpoint filters** to all endpoints in the class: `[HandlerEndpointGroup("Orders", EndpointFilters = [typeof(AuditFilter)])]` * To share auth, filter, or routing configuration across all handler methods in one place **Properties:** | Property | Purpose | | --- | --- | | `Name` | Group name used as the OpenAPI tag and default route prefix (kebab-cased). When omitted, auto-derived from the class name (e.g., `OrderHandler` → `"Orders"`) | | `RoutePrefix` | Override the route prefix (relative to global prefix; use leading `/` for absolute) | | `Tags` | Override the OpenAPI tags (defaults to `Name` as a single tag) | | `EndpointFilters` | `IEndpointFilter` types applied to all endpoints in this group | ### `[HandlerEndpoint]` — Customize Individual Endpoints `[HandlerEndpoint]` is applied to a handler **method** (or class to set defaults for all methods) and controls a single endpoint. Use it to override the route, HTTP method, OpenAPI metadata, or exclude a handler from endpoint generation. ```csharp public class TodoHandler { // Override route and HTTP method for an action endpoint [HandlerEndpoint(HandlerMethod.Post, "{todoId}/complete")] public Task HandleAsync(CompleteTodo command) { ... } // Custom OpenAPI metadata [HandlerEndpoint(Name = "BulkCreateTodos", Summary = "Creates multiple todos at once")] public Task>> HandleAsync(BulkCreateTodos command) { ... } // Exclude from endpoint generation [HandlerEndpoint(Exclude = true)] public Task HandleAsync(InternalCleanup command) { ... } } ``` **Constructor overloads:** The attribute supports four constructor forms for convenience: ```csharp [HandlerEndpoint] // Fully convention-based [HandlerEndpoint("/{productId}")] // Lock the route only [HandlerEndpoint(HandlerMethod.Get)] // Lock the HTTP method only [HandlerEndpoint(HandlerMethod.Get, "/{productId}")] // Lock both the HTTP method and route ``` All properties (including `Method` and `Route`) are also settable as named arguments for maximum flexibility: ```csharp [HandlerEndpoint(Method = HandlerMethod.Post, Route = "{todoId}/complete")] ``` **When do you need `[HandlerEndpoint]`?** * To set a **custom route** different from what's auto-generated * To override the **HTTP method** when conventions don't match (e.g., an action verb that should be POST) * To add **OpenAPI metadata** (summary, description, operation ID, tags) * To **exclude** a handler from endpoint generation * To set a specific **success status code** or explicit **error status codes** * To configure **SSE streaming** on a streaming handler * To apply **endpoint filters** to a specific endpoint **Properties:** | Property | Purpose | | --- | --- | | `Method` | Override inferred HTTP method (`HandlerMethod.Get`, `.Post`, `.Put`, `.Delete`, `.Patch`) | | `Route` | Custom route template (relative to group prefix; leading `/` for absolute) | | `Name` | OpenAPI operation ID | | `Summary` | Override XML doc summary for OpenAPI | | `Description` | OpenAPI description | | `Tags` | Override the group tags | | `Exclude` | `true` to skip endpoint generation entirely | | `EndpointFilters` | `IEndpointFilter` types for this endpoint | | `SuccessStatusCode` | Override auto-detected success status code (200, 201, etc.) | | `ProducesStatusCodes` | Explicit error status codes for OpenAPI (e.g., `[404, 400]`) | | `Streaming` | `EndpointStreaming.ServerSentEvents` for SSE; `Default` for JSON array | | `SseEventType` | SSE `event:` field name for `addEventListener` | **Class-level defaults:** When applied to a class, settings apply to all methods unless a method-level attribute overrides them: ```csharp [HandlerEndpoint(ProducesStatusCodes = [400, 500])] public class ProductHandler { // Inherits [400, 500] from class public Result Handle(CreateProduct command) { ... } // Overrides with its own set [HandlerEndpoint(ProducesStatusCodes = [404, 409])] public Result Handle(UpdateProduct command) { ... } } ``` ## How Routes Are Generated The generator builds every endpoint route through a simple four-step algorithm. Understanding these steps lets you predict exactly what route any message name will produce — and tells you when to reach for an explicit attribute instead. ### Step 1: Determine the Mode Every handler class operates in one of two modes, chosen automatically: | Mode | When it activates | Where the entity comes from | | --- | --- | --- | | **Single-message** | Class has exactly **1** handler method **and** the class name matches the message name | The message name | | **Group** | Class has **2+** methods, **or** the class name doesn't match the message | The handler class name | "Class name matches the message name" means the class name minus the `Handler`/`Consumer` suffix equals the message type name. For example, `GetOrderHandler` matches `GetOrder`, but `OrderHandler` does not. ```csharp // Single-message mode: "GetOrder" == "GetOrder" + Handler public class GetOrderHandler { public Result Handle(GetOrder query) { ... } } // Group mode: "Order" ≠ "GetOrder" (and has 3 methods) public class OrderHandler { public Result Handle(GetOrder query) { ... } public Result Handle(CreateOrder cmd) { ... } public Result Handle(CompleteOrder cmd) { ... } } ``` **Why two modes?** Single-message mode keeps things minimal — one handler, one route, no endpoint group or OpenAPI tag. Group mode creates an endpoint group from the class name so all methods in the class share a common route prefix and OpenAPI tag. ### Step 2: Determine the Entity The **entity** is the REST resource your handler operates on. It determines the base path of the route. **In group mode**, the entity comes from the handler class name: | Handler Class | Entity | Route Prefix | | --- | --- | --- | | `OrderHandler` | Order | `/orders` | | `ShoppingCartHandler` | ShoppingCart | `/shopping-carts` | | `TodoHandler` | Todo | `/todos` | The class name is stripped of its `Handler`/`Consumer` suffix, pluralized, and kebab-cased. This becomes the group route prefix — every method in the class inherits it. **In single-message mode**, there's no group prefix. The entity is extracted from the message name (after stripping the verb — see step 3) and becomes the route directly: | Message | Verb | Entity | Route | | --- | --- | --- | --- | | `GetOrder` | Get | Order | `GET /api/orders/{orderId}` | | `CreateTodo` | Create | Todo | `POST /api/todos` | | `CompleteTodo` | Complete | Todo | `POST /api/todos/{todoId}/complete` | ### Step 3: Infer the HTTP Method The first word of the message name (split at the PascalCase boundary) determines the HTTP method: | Prefix | HTTP Method | | --- | --- | | `Get`, `Find`, `Search`, `List`, `Query` | **GET** | | `Create`, `Add`, `New` | **POST** | | `Update`, `Edit`, `Modify`, `Change`, `Set` | **PUT** | | `Delete`, `Remove` | **DELETE** | | `Patch` | **PATCH** | | *Anything else* | **POST** | These CRUD prefixes are the only special-cased verbs. Everything else — `Complete`, `Approve`, `Cancel`, `Export`, `Finalize`, `Validate`, literally *any* verb — defaults to POST and becomes a route action suffix (see step 4). ### Step 4: Build the Route With the mode, entity, and HTTP method determined, the generator strips the entity from the message name. Whatever's left becomes the route. **CRUD verbs** are stripped cleanly — they map to HTTP methods and don't produce a route suffix: ```text GetOrder → strip "Get" → entity "Order" → GET /orders/{orderId} CreateOrder → strip "Create" → entity "Order" → POST /orders DeleteOrder → strip "Delete" → entity "Order" → DELETE /orders/{orderId} ``` **Action verbs** — anything that's not a CRUD prefix — are split at the first PascalCase word boundary. The first word is the action, the rest is the entity: ```text CompleteTodo → "Complete" + "Todo" → POST /todos/{todoId}/complete ArchiveOrder → "Archive" + "Order" → POST /orders/{orderId}/archive ExportOrders → "Export" + "Orders"→ POST /orders/export FinalizeOrder → "Finalize" + "Order" → POST /orders/{orderId}/finalize ``` The action verb is kebab-cased and appended as a route suffix. No hardcoded list of action verbs is needed — the generator figures it out by splitting PascalCase. **Single-word messages** (no PascalCase boundary, like `Login`, `Logout`, `Ping`) are treated as bare actions. In group mode, they become a route segment under the group prefix: ```csharp public class AuthHandler { public Result Handle(Login cmd) { ... } // → POST /api/auth/login public Result Handle(Logout cmd) { ... } // → POST /api/auth/logout } ``` ### Putting It All Together Here's the full algorithm applied to a typical handler: ```csharp public class OrderHandler { public Result Handle(GetOrder query) { ... } public Result Handle(GetOrders query) { ... } public Result Handle(CreateOrder cmd) { ... } public Result Handle(UpdateOrder cmd) { ... } public Result Handle(DeleteOrder cmd) { ... } public Result Handle(CompleteOrder cmd) { ... } public Result Handle(ExportOrders cmd) { ... } } ``` | Message | Step 1: Mode | Step 2: Entity | Step 3: HTTP | Step 4: Route | | --- | --- | --- | --- | --- | | `GetOrder` | Group (multi-method) | "Order" from class | `Get` → GET | Strip `Get`+`Order` → nothing left → `GET /api/orders/{orderId}` | | `GetOrders` | Group | "Order" from class | `Get` → GET | Strip `Get`+`Orders` → nothing left → `GET /api/orders` | | `CreateOrder` | Group | "Order" from class | `Create` → POST | Strip `Create`+`Order` → nothing left → `POST /api/orders` | | `UpdateOrder` | Group | "Order" from class | `Update` → PUT | Strip `Update`+`Order` → nothing left → `PUT /api/orders/{orderId}` | | `DeleteOrder` | Group | "Order" from class | `Delete` → DELETE | Strip `Delete`+`Order` → nothing left → `DELETE /api/orders/{orderId}` | | `CompleteOrder` | Group | "Order" from class | `Complete` → POST | Strip `Order` → "Complete" left → `POST /api/orders/{orderId}/complete` | | `ExportOrders` | Group | "Order" from class | `Export` → POST | Strip `Orders` → "Export" left → `POST /api/orders/export` | And the same entity split across separate single-message handlers: ```csharp public class GetOrderHandler { public Result Handle(GetOrder q) { ... } } public class CreateOrderHandler { public Result Handle(CreateOrder c) { ... } } public class CompleteOrderHandler { public Result Handle(CompleteOrder c) { ... } } ``` | Message | Step 1: Mode | Route | | --- | --- | --- | | `GetOrder` | Single (class matches) | `GET /api/orders/{orderId}` | | `CreateOrder` | Single (class matches) | `POST /api/orders` | | `CompleteOrder` | Single (class matches) | `POST /api/orders/{orderId}/complete` | Both approaches produce identical routes — organize your handlers however you prefer. ### Route Structure The final route is built by joining up to three levels: | Level | Source | Default | | --- | --- | --- | | 1. Global prefix | `[assembly: MediatorConfiguration(EndpointRoutePrefix = "...")]` | `"api"` | | 2. Group prefix | `[HandlerEndpointGroup("Name")]` or auto-derived from handler class | Pluralized entity, kebab-cased | | 3. Endpoint route | `[HandlerEndpoint(Route = "...")]` or auto-generated | Route params + action suffix | ```text /api/orders/{orderId}/complete ↑ ↑ ↑ ↑ │ │ │ └─ Action suffix (from "Complete" in CompleteOrder) │ │ └─ Route parameter (from OrderId property) │ └─ Group prefix (from OrderHandler → "orders") └─ Global prefix (default "api") ``` ::: tip Routes are automatically **pluralized**: `TodoHandler` → `/todos`, `CategoryHandler` → `/categories`, `PersonHandler` → `/people`. Irregular nouns are handled automatically. **Uncountable nouns** are not pluralized: `Health`, `Status`, `Data`, `Auth`, `Config`, `Settings`, `Media`, `Cache`, `Analytics`, etc. ::: ### Entity Name Normalization The generator strips common CQRS qualifiers from message names before deriving routes. This keeps routes clean regardless of your naming style: | Pattern | Example | What's stripped | Route | | ------- | ------- | --------------- | ----- | | `All` prefix | `GetAllTodos` | `All` | `GET /api/todos` | | `ById` suffix | `GetTodoById` | `ById` | `GET /api/todos/{id}` | | `Details` / `Detail` | `GetOrderDetails` | `Details` | `GET /api/orders/{id}` | | `Summary` | `GetOrderSummary` | `Summary` | `GET /api/orders/{id}` | | `Paged` / `Paginated` | `GetProductsPaged` | `Paged` | `GET /api/products` | | `List` / `Stream` | `GetTodoList` | `List` | `GET /api/todos` | | `With` | `GetTodosWithPagination` | `WithPagination` | `GET /api/todos` | Some patterns produce **route segments** instead of being stripped: | Pattern | Example | Route | | ------- | ------- | ----- | | `Count` suffix | `GetOrderCount` | `GET /api/orders/count` | | `By` | `GetOrderByEmail` | `GET /api/orders/by-email` | | `For` | `GetOrdersForCustomer` | `GET /api/orders/for-customer/{customerId}` | | `From` | `GetShipmentsFromWarehouse` | `GET /api/shipments/from-warehouse/{warehouseId}` | ::: tip `By`, `For`, and `From` produce route segments under the entity, keeping all routes grouped. For example, `GetTodoByName(string Name)` generates `GET /api/todos/by-name?name=...` — no conflict with the list route `GET /api/todos`. ::: ### Sub-Entity Routes When a message entity doesn't match the handler's group entity, it appears as a sub-route: ```csharp public class TodoHandler { public Result Handle(GetTodo query) { ... } // → GET /api/todos/{todoId} public Result Handle(GetTodoItems query) { ... } // → GET /api/todos/items public Result Handle(GetCurrentUser query) { ... } // → GET /api/todos/current-user } ``` `TodoItems` starts with the group entity `Todo`, so it's recognized as a sub-entity — only the `Items` part becomes a route segment. `CurrentUser` doesn't match the group entity at all, so it appears as a singular sub-resource. ### Absolute Routes A leading `/` on any level makes it **absolute** — it discards everything above it: ```csharp // Leading / on group prefix → bypasses global "api" prefix [HandlerEndpointGroup("Health", RoutePrefix = "/health")] public class HealthHandler { ... } // → GET /health (not /api/health) // Leading / on endpoint route → bypasses both global and group prefix [HandlerEndpoint(Route = "/status")] public Result Handle(GetStatus query) { ... } // → GET /status (not /api/products/status) ``` | Group `RoutePrefix` | Endpoint `Route` | Result (global = `"api"`) | | --- | --- | --- | | `"products"` (relative) | `"{productId}"` (relative) | `/api/products/{productId}` | | `"/health"` (absolute) | `""` (relative) | `/health` | | `"products"` (relative) | `"/status"` (absolute) | `/status` | | `"/v2/products"` (absolute) | `"{productId}"` (relative) | `/v2/products/{productId}` | ::: warning Route Stability Convention-based routes may evolve as naming heuristics improve across library versions. If your API routes are part of a **public contract** (consumed by external clients, documented in an OpenAPI spec, or pinned in integration tests), use explicit attributes to guarantee stability: ```csharp [HandlerEndpointGroup(RoutePrefix = "orders")] public class OrderHandler { [HandlerEndpoint(Route = "{orderId}")] public Result Handle(GetOrder query) { ... } [HandlerEndpoint(Route = "{orderId}/promote")] public Result Handle(PromoteOrder cmd) { ... } } ``` For internal APIs or rapid prototyping, convention-based routes are ideal. ::: ### Route Parameters Properties named `Id` or ending with `Id` automatically become route parameters: ```csharp public record GetProduct(string ProductId); // → GET /api/products/{productId} ``` ### Query Parameters For GET/DELETE requests, non-ID properties become query parameters: ```csharp public record SearchProducts(string? Category, int? MinPrice, int? MaxPrice); // → GET /api/products?category=...&minPrice=...&maxPrice=... ``` ## Converting to Explicit Endpoints By default, routes and HTTP methods are derived from conventions — message names, handler class names, and property types. This is great for rapid development, but renaming a message or refactoring a handler class can silently change your API surface. Converting to **explicit endpoints** freezes the route and HTTP method into attributes so they won't change when you refactor. ### Using the Code Fix The analyzer reports an `FMED017` info diagnostic on every handler method that generates (or could generate) an endpoint, showing the computed route: ```text Endpoint: GET /api/products/{productId} ``` Click the lightbulb (or `Ctrl+.`) to see: * **Make endpoint explicit** — adds `[HandlerEndpoint(HandlerMethod.Get, "{productId}")]` (or just `[HandlerEndpoint(HandlerMethod.Get)]` when there's no route segment) to the method and `[HandlerEndpointGroup("Products")]` to the class (if not already present) * **Make all endpoints in class explicit** — applies the same to every handler method in the class * **Fix All** → Document / Project / Solution — converts endpoints across a wider scope Once converted, the FMED017 diagnostic still shows the route but the code fix no longer appears (the endpoint is already explicit). ### Explicit Discovery Mode When `EndpointDiscovery = EndpointDiscovery.Explicit`, only handlers with `[HandlerEndpoint]` generate endpoints. The FMED017 diagnostic still appears on all handler methods — showing what the route *would* be — so you can use the code fix to opt in: ```text Endpoint: GET /api/products/{productId} (not generated — add [HandlerEndpoint] to opt in) ``` Clicking "Make endpoint explicit" adds the attribute, which simultaneously opts the handler in and freezes the route. ### Converting Manually You can also add the attributes manually using any of the constructor forms: ```csharp [HandlerEndpoint(HandlerMethod.Get, "{productId}")] // Explicit verb and route [HandlerEndpoint("{productId}")] // Explicit route only (verb still inferred) [HandlerEndpoint(HandlerMethod.Get)] // Explicit verb only (route still inferred) [HandlerEndpoint(Method = HandlerMethod.Get)] // Same, using named argument ``` ## Parameter Binding ### GET/DELETE Requests **`[AsParameters]` binding** (message has a parameterless constructor): ```csharp public record SearchProducts { public string? Category { get; init; } public int Page { get; init; } = 1; } // → MapGet("/", async ([AsParameters] SearchProducts message, ...) => ...) ``` **Constructor binding** (message has required constructor parameters): ```csharp public record GetProduct(string ProductId); // → MapGet("/{productId}", async (string productId, ...) => // { var message = new GetProduct(productId); ... }) ``` ### POST/PUT/PATCH Requests Request body is bound using `[FromBody]`. For PUT/PATCH with route parameters, the body is merged with route values: ```csharp public record UpdateProduct(string ProductId, string? Name, decimal? Price); // → MapPut("/{productId}", async (string productId, [FromBody] UpdateProduct message, ...) => // { var mergedMessage = message with { ProductId = productId }; ... }) ``` ### Binding Attributes on Message Properties You can use `[FromHeader]`, `[FromQuery]`, and `[FromRoute]` on message properties to control how individual values are bound in endpoints. This is useful for values like tenant IDs, correlation IDs, or API keys that come from HTTP headers rather than the request body. **POST with a header-bound property:** ```csharp public record CreateOrder( string CustomerId, decimal Amount, [property: FromHeader(Name = "X-Tenant-Id")] string TenantId ); ``` The generator extracts the attributed property as a separate endpoint parameter and merges it back into the message: ```csharp // Generated: MapPost("/orders", ([FromBody] CreateOrder message, [FromHeader(Name = "X-Tenant-Id")] string tenantId, HttpContext httpContext, IMediator mediator, CancellationToken ct) => { var mergedMessage = message with { TenantId = tenantId }; // ... }) ``` **GET with a header-bound property:** For GET/DELETE messages with a parameterless constructor, `[AsParameters]` binding handles header attributes natively — no extra work from the generator: ```csharp public record SearchProducts { public string? Category { get; init; } [FromHeader(Name = "X-Tenant-Id")] public string TenantId { get; init; } = ""; } // → MapGet("/", ([AsParameters] SearchProducts message, ...) => ...) // ASP.NET binds Category from query string, TenantId from header ``` ::: tip Record syntax C# records use positional parameters that default to **constructor** attribute targets. To place an attribute on the generated **property**, use the `property:` target prefix: ```csharp public record MyMessage( string Name, [property: FromHeader(Name = "X-Custom")] string CustomValue ); ``` ::: ::: info Transport-agnostic The message is still the contract. When calling via `mediator.InvokeAsync()`, you provide all values directly — the binding attributes are only used by the endpoint generator: ```csharp // From an endpoint: TenantId comes from the X-Tenant-Id header automatically // From code: you provide it explicitly await mediator.InvokeAsync(new CreateOrder("cust-1", 99.99m, "tenant-42")); ``` ::: ### Accessing HTTP Types in Handlers When a handler is invoked through a generated endpoint, `HttpContext`, `HttpRequest`, `HttpResponse`, and `ClaimsPrincipal` are automatically available as handler method parameters — no DI registration required: ```csharp public class ProductHandler { public Result Handle( GetProduct query, HttpContext httpContext) // Auto-resolved from the endpoint { var userAgent = httpContext.Request.Headers.UserAgent; // ... } public Result Handle( ExportProducts query, HttpResponse response) // Also works with HttpRequest / HttpResponse / ClaimsPrincipal directly { response.Headers.Append("X-Export-Id", Guid.NewGuid().ToString()); // ... } public Result Handle( GetCurrentUser query, ClaimsPrincipal user) // ClaimsPrincipal from HttpContext.User { return user.Identity?.Name ?? "anonymous"; } } ``` The endpoint generator populates a `CallContext` with these values before calling the handler. When the same handler is invoked directly via `mediator.InvokeAsync()` (without an endpoint), these parameters fall back to normal DI resolution. ::: warning Avoid HTTP types in handlers when possible Using `HttpContext`, `HttpRequest`, `HttpResponse`, or `ClaimsPrincipal` in a handler **couples it to HTTP** — the handler can only work when called from an endpoint, and it becomes harder to unit test (you need to mock or construct HTTP types). Prefer putting all the data you need into your **message type** instead: ```csharp // ❌ Coupled to HTTP — hard to test, only works from endpoints public Result Handle(GetProduct query, HttpContext httpContext) { var tenant = httpContext.Request.Headers["X-Tenant-Id"].ToString(); // ... } // ✅ Transport-agnostic — easy to test, works from anywhere public record GetProduct(string Id, string TenantId); public Result Handle(GetProduct query) { // query.TenantId is populated by middleware or the endpoint binding } ``` Reserve HTTP type parameters for edge cases where you genuinely need low-level HTTP access (e.g., streaming a response body, reading raw headers that can't be modeled as message properties). ::: ## Result to HTTP Status Mapping `Result` and `Result` return values are automatically mapped to HTTP responses: | ResultStatus | HTTP Status | | ------------ | ----------- | | `Ok` | 200 OK | | `Created` | 201 Created | | `NoContent` | 204 No Content | | `BadRequest` | 400 Bad Request | | `Invalid` | 400 Bad Request (ValidationProblem) | | `NotFound` | 404 Not Found | | `Unauthorized` | 401 Unauthorized | | `Forbidden` | 403 Forbidden | | `Conflict` | 409 Conflict | | `Error` | 500 Internal Server Error | | `CriticalError` | 500 Internal Server Error | | `Unavailable` | 503 Service Unavailable | ### Custom Result Mapping To customize how `Result` statuses are converted to HTTP responses, implement `IMediatorResultMapper` and register it before `AddMediator()`: ```csharp using Microsoft.AspNetCore.Http; public class CustomResultMapper : IMediatorResultMapper { public IResult MapResult(Foundatio.Mediator.IResult result) => result.Status switch { // Use ProblemDetails for NotFound instead of the default anonymous object ResultStatus.NotFound => Results.Problem( detail: result.Message, statusCode: 404, title: "Not Found"), ResultStatus.BadRequest => Results.Problem( detail: result.Message, statusCode: 400, title: "Bad Request"), // Handle all other statuses _ => Results.Problem(result.Message ?? "An unexpected error occurred", statusCode: 500) }; } ``` ```csharp // Register before AddMediator — your implementation takes priority services.AddSingleton, CustomResultMapper>(); services.AddMediator(); ``` When no custom mapper is registered, the generated default handles all `ResultStatus` values with sensible HTTP status codes. ### File Downloads `Result` automatically produces a file response: ```csharp public class ReportHandler { [HandlerEndpoint(HandlerMethod.Get, "/reports/{id}")] public async Task> HandleAsync( GetReport query, IReportService reports, CancellationToken ct) { var stream = await reports.GeneratePdfAsync(query.Id, ct); return Result.File(stream, "application/pdf", $"report-{query.Id}.pdf"); } } ``` ### OpenAPI Error Responses The generator scans your handler body for `Result` factory calls and emits matching `.ProducesProblem()` metadata automatically: ```csharp public class OrderHandler { public Result Handle(GetOrder query) { if (query.Id == null) return Result.NotFound("Order not found"); // → .ProducesProblem(404) if (!IsValid(query)) return Result.Invalid("Bad request"); // → .ProducesProblem(400) return new OrderView(query.Id, "Test"); } // Auto-generates: .Produces(200), .ProducesProblem(404), .ProducesProblem(400) } ``` ### Success Status Codes If a handler body contains `Result.Created()`, the endpoint is generated with **201 Created**; otherwise it defaults to **200 OK**. Override with `[HandlerEndpoint(SuccessStatusCode = 201)]`. ## Authentication & Authorization Authorization works for **both** HTTP endpoints and direct `mediator.InvokeAsync()` calls, configured via `[HandlerAuthorize]` and `[HandlerAllowAnonymous]`: ```csharp // Require auth on a handler [HandlerAuthorize(Roles = ["Admin", "Manager"])] public class AdminHandler { public Task HandleAsync(DeleteProduct command) { ... } } // Require auth globally [assembly: MediatorConfiguration(AuthorizationRequired = true)] // Opt out specific handlers — either attribute works [HandlerAllowAnonymous] // or [AllowAnonymous] from Microsoft.AspNetCore.Authorization public class PublicHandler { public Task> HandleAsync(HealthCheck query) { ... } } ``` Authorization cascades: assembly defaults → group level → method level. For Result-returning handlers, unauthorized requests receive `Result.Unauthorized()` or `Result.Forbidden()`. For non-Result handlers, an `UnauthorizedAccessException` is thrown. The authorization system is extensible via `IAuthorizationContextProvider` (provides the `ClaimsPrincipal`) and `IHandlerAuthorizationService` (performs the auth check). ASP.NET Core apps get automatic registration that reads from `HttpContext.User`. ## Discovery Modes Control which handlers generate endpoints: | Mode | Behavior | | --- | --- | | `EndpointDiscovery.All` (default) | All handlers get endpoints; use `[HandlerEndpoint(Exclude = true)]` to opt out | | `EndpointDiscovery.Explicit` | Only handlers with `[HandlerEndpoint]` or `[HandlerEndpointGroup]` get endpoints | | `EndpointDiscovery.None` | No endpoints generated | ```csharp [assembly: MediatorConfiguration(EndpointDiscovery = EndpointDiscovery.Explicit)] ``` ## Events and Notifications Handlers for event/notification types are automatically excluded from endpoint generation: * Types implementing `INotification`, `IEvent`, `IDomainEvent`, or `IIntegrationEvent` * Handler classes named `*EventHandler` or `*NotificationHandler` * Types with names ending in event suffixes: `Created`, `Updated`, `Deleted`, `Changed`, `Removed`, `Added`, `Event`, `Notification`, `Published`, `Occurred`, `Happened`, `Started`, `Completed`, `Failed`, `Cancelled`, `Expired` ::: info INotification Is Not Required Events are excluded based on naming conventions regardless of interface implementation. `INotification` is a classification tool — use it when you want a handler that can receive all notification-type messages, or simply as self-documentation. ::: ## OpenAPI / XML Documentation XML doc `` comments on handler methods automatically become the OpenAPI summary for the generated endpoint. Enable documentation generation in your project file: ```xml true $(NoWarn);CS1591 ``` Then add `` comments to your handler methods: ```csharp public class ProductHandler { /// Create a new product in the catalog public Task> HandleAsync(CreateProduct command) { ... } /// Get a product by its unique identifier public Result Handle(GetProduct query) { ... } /// List all products with optional filtering public Result> Handle(GetProducts query) { ... } } ``` The generated endpoints will include these summaries in their OpenAPI metadata — visible in Swagger UI, Scalar, and any other OpenAPI tooling. You can also override the summary per-endpoint using `[HandlerEndpoint(Summary = "...")]`. ## Advanced Configuration ### Assembly Endpoint Options ```csharp app.MapMediatorEndpoints(c => { c.AddAssembly(); // Products.Module c.AddAssembly(); // Orders.Module c.LogEndpoints(); // Log all mapped routes at startup }); ``` ### Global Settings ```csharp [assembly: MediatorConfiguration( EndpointDiscovery = EndpointDiscovery.All, EndpointRoutePrefix = "api", // Global route prefix (default: "api") AuthorizationRequired = false, // Require auth for all endpoints EndpointFilters = [typeof(MyFilter)] // Global endpoint filters )] ``` Set `EndpointRoutePrefix = ""` to disable the global prefix entirely. ## Endpoint Conventions Endpoint conventions let you create reusable attributes that configure generated endpoints at startup — rate limiting, CORS, caching headers, or any other endpoint builder customization. Implement `IEndpointConvention` on an attribute and the source generator handles the rest. No runtime reflection. ### Defining a Convention Create an attribute that implements `IEndpointConvention`: ```csharp [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Method | AttributeTargets.Class)] public sealed class RateLimitedAttribute : Attribute, IEndpointConvention { public string? PolicyName { get; } public RateLimitedAttribute(string? policyName = null) => PolicyName = policyName; public void Configure(RouteHandlerBuilder builder) { builder.RequireRateLimiting(PolicyName ?? "default"); } } ``` The `Configure` method receives the endpoint builder at startup and can call any builder extension method — `RequireRateLimiting`, `RequireCors`, `CacheOutput`, `WithMetadata`, etc. ### Applying Conventions Conventions can be applied at three scopes: ```csharp // Assembly level — applies to ALL endpoints in this assembly [assembly: RateLimited] // Class level — applies to all endpoints in this handler [RateLimited("moderate")] public class OrderHandler { // Method level — applies to this endpoint only [RateLimited("strict")] public Task> HandleAsync(CreateOrder cmd) { ... } // Inherits class-level "moderate" policy public Task> HandleAsync(GetOrder query) { ... } } ``` ### Most-Derived Wins When the same convention attribute appears at multiple scopes, the most specific one wins: | Scope | Priority | | --- | --- | | **Method** | Highest — overrides class and assembly | | **Class** | Overrides assembly | | **Assembly** | Lowest — global default | This lets you set a global default and override it where needed: ```csharp // Global default: all endpoints get "default" rate limit [assembly: RateLimited] public class ProductHandler { // Inherits assembly default → "default" policy public Result> Handle(GetProducts query) { ... } // Overrides with "strict" for write operations [RateLimited("strict")] public Task> HandleAsync(CreateProduct cmd) { ... } } ``` For each attribute type, only one scope applies per endpoint — there's no stacking. If you need multiple independent behaviors, use separate attribute types. ### Group Conventions To configure the `RouteGroupBuilder` (shared prefix group) instead of individual endpoints, implement `IEndpointConvention`: ```csharp [AttributeUsage(AttributeTargets.Class)] public class GroupCorsAttribute : Attribute, IEndpointConvention { public string PolicyName { get; } = "default"; public GroupCorsAttribute(string? policyName = null) => PolicyName = policyName ?? "default"; public void Configure(RouteGroupBuilder group) { group.RequireCors(PolicyName); } } [HandlerEndpointGroup("Orders")] [GroupCors("allow-frontend")] public class OrderHandler { ... } ``` Group conventions applied at the class level configure the group builder, affecting all endpoints in that group. ### How It Works The source generator: 1. Detects attributes implementing `IEndpointConvention` on methods, classes, and the assembly 2. Records their constructor arguments and named properties 3. Emits code that reconstructs each attribute and calls `Configure(builder)` at startup No reflection is used at runtime. The generated code looks like: ```csharp // Generated — you never write this var ep = ordersGroup.MapPost("", async (...) => { ... }); ((IEndpointConvention)new RateLimitedAttribute("strict")).Configure(ep); ``` ## Troubleshooting ### Endpoints Not Generated 1. Ensure your project references ASP.NET Core (has `Microsoft.AspNetCore.Routing`) 2. Check `[assembly: MediatorConfiguration(EndpointDiscovery = ...)]` — default is `All`. Make sure it hasn't been set to `None` or `Explicit`. 3. In `Explicit` mode, handlers need `[HandlerEndpoint]` or `[HandlerEndpointGroup]` 4. Verify the handler isn't excluded via `[HandlerEndpoint(Exclude = true)]` ### XML Summaries Not Appearing 1. Enable `true` 2. Rebuild the project completely ### Route Conflicts When multiple handlers generate the same route, the generator automatically differentiates them using the message type name in kebab-case. --- --- url: /guide/events-and-notifications.md --- # Events & Notifications Events are one of the most powerful features of Foundatio Mediator. They let you build **loosely coupled systems** where code reacts to things happening elsewhere — without direct references between the producer and the consumers. ## Publishing Events Use `PublishAsync` to broadcast a message to all matching handlers: ```csharp await mediator.PublishAsync(new OrderCreated("ORD-001", DateTime.UtcNow)); ``` **By default, `PublishAsync` waits for all handlers to complete before returning.** This is a deliberate design choice — it means you can reliably add event handlers and know they will run to completion before the publishing code continues. Unlike fire-and-forget systems, you don't lose events or race against request lifetimes. Any message type works — events don't require special interfaces: ```csharp public record OrderCreated(string OrderId, DateTime CreatedAt); ``` ## Handling Events Any handler discovered by the source generator can handle published events. Multiple handlers can handle the same event: ```csharp public class EmailHandler { public Task HandleAsync(OrderCreated e, IEmailService email) => email.SendOrderConfirmationAsync(e.OrderId); } public class AuditHandler { public void Handle(OrderCreated e, ILogger logger) => logger.LogInformation("Order {OrderId} created at {Time}", e.OrderId, e.CreatedAt); } public class InventoryHandler { public Task HandleAsync(OrderCreated e, IInventoryService inventory) => inventory.ReserveItemsAsync(e.OrderId); } ``` All three handlers run when `OrderCreated` is published. The publishing code doesn't know or care which handlers exist — you can add, remove, or reorder them without touching the publisher. ## The INotification Interface `INotification` is a built-in marker interface for classifying event types: ```csharp public record OrderCreated(string OrderId, DateTime CreatedAt) : INotification; public record OrderShipped(string OrderId) : INotification; ``` It's completely **optional** — plain records work fine with `PublishAsync`. But it's useful for: * **Self-documenting code** — makes it clear a type is an event, not a command or query * **Interface subscriptions** — subscribe to all notifications with `SubscribeAsync()` * **Middleware filtering** — apply middleware only to notification types You can also define your own marker interfaces for more specific grouping: ```csharp public interface IDispatchToClient { } public record OrderCreated(string OrderId) : INotification, IDispatchToClient; public record ProductUpdated(string ProductId) : INotification, IDispatchToClient; public record AuditEntry(string Action) : INotification; // Not dispatched to clients ``` ## Handler Execution Order When multiple handlers handle the same event, you can control the order they run: ```csharp [Handler(Order = 1)] public class ValidationHandler { public void Handle(OrderCreated evt) { /* Runs first */ } } [Handler(Order = 2)] public class InventoryHandler { public void Handle(OrderCreated evt) { /* Runs second */ } } // No Order specified — runs last (default is int.MaxValue) public class NotificationHandler { public void Handle(OrderCreated evt) { /* Runs last */ } } ``` You can also express ordering relationships without numeric values: ```csharp [Handler(OrderBefore = [typeof(NotificationHandler)])] public class InventoryHandler { public void Handle(OrderCreated evt) { /* Runs before NotificationHandler */ } } ``` See [Handler Conventions](./handler-conventions#handler-execution-order) for details on ordering and relative ordering. ## Publish Strategies The default strategy (`ForeachAwait`) runs handlers sequentially and waits for each to complete. You can change this globally: | Strategy | Behavior | Use Case | |----------|----------|----------| | **`ForeachAwait`** (default) | Sequential, waits for each handler | Predictable ordering, reliable completion | | **`TaskWhenAll`** | Concurrent, waits for all to complete | Maximum throughput for independent handlers | | **`FireAndForget`** | Concurrent, returns immediately | Background work where you don't need completion guarantees | Configure via the assembly attribute: ```csharp // Assembly attribute [assembly: MediatorConfiguration( NotificationPublishStrategy = NotificationPublishStrategy.TaskWhenAll)] ``` ::: warning `FireAndForget` swallows exceptions and handlers may outlive the caller. Use with caution. ::: ## Error Handling When a handler throws during `PublishAsync`, the behavior depends on the publish strategy: * **`ForeachAwait`** — remaining handlers still execute. After all handlers complete, an `AggregateException` is thrown containing all failures. * **`TaskWhenAll`** — all handlers run concurrently. Failures are collected and thrown as an `AggregateException`. * **`FireAndForget`** — exceptions are swallowed. This means a failing handler never prevents other handlers from running. ## Cascading Events Instead of calling `PublishAsync` explicitly, handlers can return events as tuple values. The mediator automatically publishes the extra values: ```csharp public class OrderHandler { public (Result, OrderCreated?) Handle(CreateOrder command) { var order = CreateOrder(command); return (order, new OrderCreated(order.Id, DateTime.UtcNow)); } } ``` The `OrderCreated` event is published automatically after the handler returns. See [Cascading Messages](./cascading-messages) for the full API including conditional events and multi-event tuples. ## Dynamic Subscriptions For scenarios where you need to consume events **at runtime** rather than through static handlers — such as streaming to connected clients — use `SubscribeAsync`: ```csharp await foreach (var evt in mediator.SubscribeAsync(cancellationToken)) { Console.WriteLine($"Order created: {evt.OrderId}"); } ``` Subscribe to an interface to receive all matching types: ```csharp await foreach (var evt in mediator.SubscribeAsync(cancellationToken)) { // Receives OrderCreated, ProductUpdated, etc. } ``` Each subscriber gets its own buffered channel. Configure buffer behavior with `SubscriberOptions`: ```csharp await foreach (var evt in mediator.SubscribeAsync( cancellationToken, new SubscriberOptions { MaxCapacity = 50 })) { // ... } ``` There is zero cost when nobody is subscribed — `PublishAsync` skips the subscription fan-out entirely. For combining dynamic subscriptions with SSE streaming endpoints, see [Streaming Handlers](./streaming-handlers#dynamic-subscriptions-with-subscribeasync). ## Best Practices * **Use `PublishAsync` for events, `InvokeAsync` for commands/queries** — events go to many handlers, commands go to exactly one * **Keep events small and focused** — include only the data consumers need, not entire entities * **Use nullable tuple types for conditional cascading** — `(Result, OrderCreated?)` lets you skip publishing on error paths cleanly * **Stick with the default publish strategy** unless you have a specific reason to change it — sequential execution with guaranteed completion is the safest default for a loosely coupled system --- --- 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 Most .NET apps start simple and gradually become a tangled web of services calling services. Foundatio Mediator helps you avoid that from day one. Instead of components calling each other directly, every interaction flows through messages — so your code stays loosely coupled, easy to test, and easy to understand as it grows. You get all of this with near-direct-call performance and zero boilerplate. Get up and running in under a minute. ## Quick Start ### 1. Install the package ```bash dotnet add package Foundatio.Mediator ``` ### 2. Define a message and handler ```csharp // A message is just a record (or class) public record Ping(string Text); // Any class ending in "Handler" is discovered automatically public class PingHandler { public string Handle(Ping msg) => $"Pong: {msg.Text}"; } ``` ### 3. Wire up DI and call it ::: code-group ```csharp [ASP.NET Core] var builder = WebApplication.CreateBuilder(args); builder.Services.AddMediator(); var app = builder.Build(); app.MapGet("/ping", (IMediator mediator) => mediator.Invoke(new Ping("Hello"))); app.Run(); ``` ```csharp [Console / Worker] var builder = Host.CreateApplicationBuilder(args); builder.Services.AddMediator(); var host = builder.Build(); var mediator = host.Services.GetRequiredService(); var result = mediator.Invoke(new Ping("Hello")); Console.WriteLine(result); // Pong: Hello ``` ::: That's it. No interfaces, no base classes, no registration — the source generator handles everything at compile time with near-direct-call performance. ::: tip Zero Configuration Required The library tries to do the right thing by default — discovery, lifetimes, routing, and endpoints all just work. Configuration options exist only as an escape hatch when you need more control. See [Configuration](./configuration) for the full list. ::: ## Async Handlers Handlers can be async and accept additional parameters resolved from DI: ```csharp public record GetUser(int Id); public class UserHandler { public async Task HandleAsync(GetUser query, IUserRepository repo, CancellationToken ct) { return await repo.GetByIdAsync(query.Id, ct); } } ``` ```csharp var user = await mediator.InvokeAsync(new GetUser(42)); ``` The first parameter is always the message. Everything else — services, `CancellationToken` — is injected automatically. See [Handler Conventions](./handler-conventions) for the full set of discovery rules, method names, and signature options. ## Generate API Endpoints Endpoints are generated automatically for all handlers: ```csharp public record CreateTodo(string Title); public record GetTodo(string Id); public class TodoHandler { public Todo Handle(CreateTodo cmd) => new(Guid.NewGuid().ToString(), cmd.Title); public Todo Handle(GetTodo query) => new(query.Id, "Sample"); } ``` ```csharp // Program.cs app.MapMediatorEndpoints(); ``` This generates: * `POST /api/todos` → `TodoHandler.Handle(CreateTodo)` * `GET /api/todos/{id}` → `TodoHandler.Handle(GetTodo)` HTTP methods, routes, and parameter binding are all inferred from message names and properties. Routes derive from the message name, are auto-pluralized, and common qualifiers like `All` and `ById` are normalized. See [Endpoints](./endpoints) for route customization, naming conventions, and more. ## Result Types Return `Result` instead of throwing exceptions for expected failures: ```csharp public class TodoHandler { public Result Handle(GetTodo query, ITodoRepository repo) { var todo = repo.Find(query.Id); if (todo is null) return Result.NotFound($"Todo {query.Id} not found"); return todo; // implicit conversion to Result } } ``` When used with generated endpoints, `Result` maps automatically to the correct HTTP status code — `200`, `404`, `400`, `409`, etc. See [Result Types](./result-types) for the full API. ## Events Publish messages to multiple handlers with `PublishAsync`: ```csharp public record OrderCreated(string OrderId, DateTime CreatedAt) : INotification; // Both handlers run when OrderCreated is published public class EmailHandler { public Task HandleAsync(OrderCreated e, IEmailService email) => email.SendAsync($"Order {e.OrderId} confirmed"); } public class AuditHandler { public void Handle(OrderCreated e, ILogger logger) => logger.LogInformation("Order {OrderId} created at {Time}", e.OrderId, e.CreatedAt); } ``` ```csharp await mediator.PublishAsync(new OrderCreated("ORD-001", DateTime.UtcNow)); ``` By default, `PublishAsync` waits for all handlers to complete — so you can reliably add event handlers knowing they'll run before the publisher continues. See [Events & Notifications](./events-and-notifications) for the full story on publish strategies, error handling, and dynamic subscriptions. ### Dynamic Subscriptions Subscribe to published notifications as an async stream — and combine with a streaming handler to get a real-time SSE endpoint in just a few lines: ```csharp public class EventStreamHandler(IMediator mediator) { [HandlerEndpoint(Streaming = EndpointStreaming.ServerSentEvents)] public async IAsyncEnumerable Handle(GetEventStream message, [EnumeratorCancellation] CancellationToken ct) { await foreach (var evt in mediator.SubscribeAsync(ct)) yield return evt; } } ``` Every notification published anywhere in the app is now pushed to connected clients over SSE. You can use `SubscribeAsync` to subscribe to any type, including interfaces; any published messages assignable to the type parameter will be delivered. See [Dynamic Subscriptions](./streaming-handlers#dynamic-subscriptions-with-subscribeasync) for the full API. ## Middleware Add cross-cutting concerns by creating classes ending in `Middleware`: ```csharp public class LoggingMiddleware { public void Before(object message, ILogger logger) => logger.LogInformation("→ {MessageType}", message.GetType().Name); public void After(object message, ILogger logger) => logger.LogInformation("← {MessageType}", message.GetType().Name); } ``` Middleware supports `Before`, `After`, `Finally`, and `ExecuteAsync` hooks with state passing, ordering, and short-circuiting. See [Middleware](./middleware) for the full pipeline. ## Cross-Assembly Handlers In multi-project solutions, register assemblies so handlers in referenced projects are discovered: ```csharp builder.Services.AddMediator(c => c .AddAssembly() // Orders.Module .AddAssembly() // Products.Module ); ``` See [Clean Architecture](./clean-architecture) for a complete modular monolith example. ## Next Steps | Topic | Description | | ----- | ----------- | | [Handler Conventions](./handler-conventions) | All discovery rules, method names, static handlers, explicit attributes | | [Events & Notifications](./events-and-notifications) | Publish/subscribe, cascading messages, dynamic subscriptions | | [Authorization](./authorization) | Built-in auth for endpoints and direct mediator calls, policies, roles | | [Dependency Injection](./dependency-injection) | Lifetimes, parameter injection, constructor vs method injection | | [Result Types](./result-types) | `Result` API, status codes, validation errors | | [Middleware](./middleware) | Pipeline hooks, ordering, state passing, short-circuiting | | [Endpoints](./endpoints) | Route conventions, OpenAPI, authorization, filters | | [Configuration](./configuration) | All compile-time and runtime options | | [Streaming Handlers](./streaming-handlers) | `IAsyncEnumerable` support and dynamic subscriptions | | [Performance](./performance) | Benchmarks and how interceptors work | | [Clean Architecture](./clean-architecture) | Modular monolith example with cross-assembly handlers | | [Troubleshooting](./troubleshooting) | Common issues and solutions | ::: info LLM-Friendly Docs For AI assistants, we provide [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt) following the [llmstxt.org](https://llmstxt.org/) standard. ::: --- --- 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. ## Why Conventions? Some developers initially feel that convention-based discovery is "too magical." But consider: **all abstractions are magic until you learn them.** * ASP.NET discovers `*Controller` classes automatically * Entity Framework treats `Id` properties as primary keys * xUnit runs methods decorated with `[Fact]` * C# records generate equality, `ToString()`, and more from a single line The real question isn't "is it magic?" but **"does the magic help or hurt?"** Convention-based handlers provide tangible benefits over traditional interface-based approaches: | Benefit | Convention-Based | Interface-Based | | ------- | ---------------- | --------------- | | **Multiple handlers per class** | ✅ Natural grouping | ❌ One interface per handler | | **Flexible method signatures** | ✅ Any params via DI | ❌ Fixed `Handle(TRequest, CancellationToken)` | | **Sync or async** | ✅ Return `void`, `T`, `Task`, etc. | ❌ Must return `Task` | | **Static handlers** | ✅ Zero allocation | ❌ Always instance-based | | **Cascading messages** | ✅ Tuple returns auto-publish | ❌ Manual `IPublisher` injection | | **Boilerplate** | ✅ Just a class + methods | ❌ Interface inheritance per handler | **Prefer explicit declaration?** Use `IHandler` or `[Handler]` attributes. See [Explicit Handler Declaration](#explicit-handler-declaration). Alternatively, you can mark handlers explicitly using the `IHandler` marker interface or the `[Handler]` attribute. See [Explicit Handler Declaration](#explicit-handler-declaration) for details. ## Message Type Interfaces Foundatio Mediator provides optional marker interfaces for messages. These enable type inference at call sites: ### IRequest\ The base interface for messages that return a response: ```csharp public record GetUser(int Id) : IRequest; // Type inference - no need to specify User user = await mediator.InvokeAsync(new GetUser(123)); ``` ### ICommand\ For commands (operations that change state): ```csharp public record CreateUser(string Name, string Email) : ICommand; // Type inference works User user = await mediator.InvokeAsync(new CreateUser("John", "john@example.com")); ``` ### IQuery\ For queries (read-only operations): ```csharp public record FindUsers(string SearchTerm) : IQuery>; // Type inference works List users = await mediator.InvokeAsync(new FindUsers("john")); ``` ### Non-Generic Interfaces For messages without responses, use the non-generic versions: ```csharp public record SendEmail(string To, string Body) : ICommand; // No response public record LogEvent(string Message) : INotification; // Multiple handlers ``` ::: tip When to Use These Interfaces These interfaces are **optional**. Use them when you want: * Type inference at call sites (no need to specify ``) * Self-documenting message intent (command vs query vs notification) * Message classification — e.g., a handler can accept `INotification` to handle all notification types * Compatibility with MediatR-style patterns Plain messages (like `public record Ping(string Text);`) work fine without any interface. In particular, `INotification` is not required for events to work with `PublishAsync` or cascading messages — it serves as a classification/self-documentation tool. ::: ## Class Naming Conventions Handler classes must end with one of these suffixes: * **`Handler`** — the preferred convention for all handler classes * `Consumer` — supported for convenience when migrating from other libraries (e.g., MassTransit), but `Handler` is recommended for new code ```csharp // ✅ Recommended public class UserHandler { } public class OrderHandler { } public class EventHandler { } // ✅ Also valid (migration convenience) public class EmailConsumer { } public class NotificationConsumer { } // ❌ Invalid - won't be discovered public class UserService { } public class OrderProcessor { } ``` ::: tip Use Handler for Everything Unlike some libraries that distinguish between "handlers" and "consumers," Foundatio Mediator treats both identically. The `Consumer` suffix exists purely to ease migration from other mediator libraries. For new projects, use `Handler` consistently for commands, queries, and events alike. ::: ## Method Naming Conventions Handler methods must use one of these names: * **`Handle`** / **`HandleAsync`** — the preferred convention * `Handles` / `HandlesAsync` — alternative form * `Consume` / `ConsumeAsync` — migration convenience (same as class suffix) * `Consumes` / `ConsumesAsync` — migration convenience ```csharp public class UserHandler { // ✅ Recommended public User Handle(GetUser query) { } public Task HandleAsync(GetUser query) { } // ✅ Also valid (all are functionally identical) public User Handles(GetUser query) { } public User Consume(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** - resolved from the current DI scope * **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 internally cached by default (not registered with DI). Constructor dependencies are resolved once and shared across all invocations. ### Open Generic Handlers Handlers can be declared as open generic classes and will be automatically closed for the concrete message type at runtime. This lets you build reusable handler logic that applies to many message types. ```csharp // Generic command definitions public record UpdateEntity(T Entity); public record UpdateRelation(TLeft Left, TRight Right); // Open generic handler (single generic parameter) public class EntityHandler { public Task HandleAsync(UpdateEntity command, CancellationToken ct) { // process update for entity of type T return Task.CompletedTask; } } // Open generic handler (two generic parameters) public class RelationHandler { public Task HandleAsync(UpdateRelation command, CancellationToken ct) { return Task.CompletedTask; } } // Usage await mediator.InvokeAsync(new UpdateEntity(order)); await mediator.InvokeAsync(new UpdateRelation(user, role)); ``` Guidelines: * The handler class, not the method, must be generic (generic handler methods are not currently supported). * The message type must use the handler's generic parameters (e.g., `UpdateEntity` in `EntityHandler`). * Open generic handlers participate in normal invocation rules: exactly one match required for `Invoke / InvokeAsync`; multiple open generics for the same message generic definition will cause an error when invoking (publish supports multiple). Performance Notes: * First invocation of a new closed generic combination incurs a small reflection cost; subsequent calls are cached. * Static middleware resolution still applies and middleware can itself be generic. If you need a custom behavior per entity type later, you can still add a concrete handler; the more specific (closed) handler will coexist. ## 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 (Internally Cached) ```csharp public class UserHandler { private readonly ILogger _logger; // ⚠️ Resolved once, shared across all calls public UserHandler(ILogger logger) { _logger = logger; // Cached 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 the assembly attribute to auto-register handlers: ```csharp [assembly: MediatorConfiguration(HandlerLifetime = MediatorLifetime.Scoped)] ``` Options: `Default` (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 `IHandler` interface for manual discovery: ```csharp public class UserProcessor : IHandler { public User Handle(GetUser query) { } // ✅ Discovered } ``` Handler classes and methods can be marked with the `[Handler]` attribute for manual discovery: ```csharp public class UserProcessor { [Handler] public User Process(GetUser query) { } // ✅ Discovered } ``` ## Compile-Time Validation The source generator provides compile-time errors for: ### 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) { /* ... */ } } ``` ## Explicit Handler Declaration In addition to naming conventions, handlers can be explicitly declared using: 1. **Interface** - Classes implementing the `IHandler` marker interface 2. **Attribute** - Classes or methods decorated with `[Handler]` ```csharp // Discovered via IHandler interface public class OrderProcessor : IHandler { public Order Handle(CreateOrder command) { } } // Discovered via [Handler] attribute on class [Handler] public class EmailService { public void Handle(SendEmail command) { } } // Discovered via [Handler] attribute on method public class NotificationService { [Handler] public void Process(SendNotification command) { } } ``` ### Disabling Conventional Discovery If you prefer explicit handler declaration over naming conventions, you can disable conventional discovery entirely: ```csharp [assembly: MediatorConfiguration(HandlerDiscovery = HandlerDiscovery.Explicit)] ``` When set to `Explicit`, only handlers that implement `IHandler` or have the `[Handler]` attribute are discovered. Classes with names ending in `Handler` or `Consumer` will not be automatically discovered. ## Handler Execution Order When using `PublishAsync` to broadcast messages to multiple handlers, you can control the execution order using the `Order` property on the `[Handler]` attribute: ```csharp // Handlers execute in order: 1, 2, 3, then handlers without explicit order (default) [Handler(Order = 1)] public class HighPriorityHandler { public void Handle(OrderCreated evt) { // Executes first - e.g., validate order data } } [Handler(Order = 2)] public class MediumPriorityHandler { public void Handle(OrderCreated evt) { // Executes second - e.g., update inventory } } [Handler(Order = 3)] public class LowPriorityHandler { public void Handle(OrderCreated evt) { // Executes third - e.g., send notifications } } // No Order specified - executes last (default is int.MaxValue) public class DefaultOrderHandler { public void Handle(OrderCreated evt) { // Executes after all handlers with explicit Order } } ``` ### Order Property Details * **Lower values execute first** - A handler with `Order = 1` executes before `Order = 10` * **Default order is `int.MaxValue`** - Handlers without explicit order execute last * **Only affects `PublishAsync`** - `InvokeAsync` always invokes exactly one handler * **Two syntax options:** * Constructor argument: `[Handler(5)]` * Named property: `[Handler(Order = 5)]` ### Relative Ordering Instead of managing numeric order values, you can express ordering relationships between handlers using `OrderBefore` and `OrderAfter`: ```csharp public record OrderCreated(string OrderId); // "I must run before NotificationHandler" [Handler(OrderBefore = [typeof(NotificationHandler)])] public class InventoryHandler { public void Handle(OrderCreated evt) { // Update inventory first } } // "I must run after InventoryHandler" [Handler(OrderAfter = [typeof(InventoryHandler)])] public class NotificationHandler { public void Handle(OrderCreated evt) { // Send notification after inventory is updated } } ``` You can specify multiple types: ```csharp // This handler must run after both Validation and Inventory handlers [Handler(OrderAfter = [typeof(ValidationHandler), typeof(InventoryHandler)])] public class AuditHandler { public void Handle(OrderCreated evt) { } } ``` **How relative ordering works:** * `OrderBefore = [typeof(X)]` means "I run before X" * `OrderAfter = [typeof(X)]` means "I run after X" * When no relative constraints exist between two handlers, numeric `Order` is used as a tiebreaker * Unknown types in `OrderBefore`/`OrderAfter` are silently ignored ::: warning Circular Dependencies If handlers form a circular dependency (e.g., A says OrderBefore B, and B says OrderBefore A), a compiler warning `FMED012` is emitted and the cycle participants fall back to numeric `Order` sorting. ::: ### Use Cases Handler ordering is useful for scenarios like: 1. **Validation before processing** - Ensure validation handlers run first 2. **Audit logging** - Record events before or after processing 3. **Cascading updates** - Update parent entities before children 4. **Priority-based notifications** - Send critical notifications first ```csharp public record PaymentReceived(string OrderId, decimal Amount); [Handler(Order = 1)] public class PaymentValidationHandler { public void Handle(PaymentReceived evt) { // First: Validate payment data } } [Handler(Order = 2)] public class OrderStatusHandler { public void Handle(PaymentReceived evt) { // Second: Update order status } } [Handler(Order = 3)] public class CustomerNotificationHandler { public void Handle(PaymentReceived evt) { // Third: Send customer confirmation } } [Handler(Order = 100)] public class AnalyticsHandler { public void Handle(PaymentReceived evt) { // Last (among explicit orders): Record analytics } } ``` ## 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` - Runs before the handler; can short-circuit execution * `After` / `AfterAsync` - Runs after successful handler completion * `Finally` / `FinallyAsync` - Always runs, even if handler fails; receives exception * `ExecuteAsync` - Wraps the entire pipeline (for retry, circuit breaker, etc.) ### Method Parameters * **First parameter**: The message (can be `object`, interface, or concrete type) * **Remaining parameters**: Injected via DI or from built-in context (see below) ### Built-in Parameter Types The following parameter types are automatically available in middleware methods without DI registration: | Parameter Type | Availability | Description | |----------------|--------------|-------------| | `CancellationToken` | Before, After, Finally, Execute | The cancellation token passed to the handler | | `IServiceProvider` | Before, After, Finally, Execute | The service provider for the current scope | | `Activity?` | Before, After, Finally, Execute | The OpenTelemetry activity (when OpenTelemetry is enabled) | | `Exception?` | **After, Finally only** | The exception if the handler failed (always `null` in Before) | | `HandlerExecutionInfo` | Before, After, Finally, Execute | Metadata about the executing handler (handler type and method) | | `HandlerExecutionDelegate` | **ExecuteAsync only** | Delegate to invoke the wrapped pipeline | | Handler return type | **After, Finally only** | The result returned by the handler (e.g., `Result`, custom types) | | Before method return type | **After, Finally only** | Values returned from the `Before` method | **Example with built-in parameters:** ```csharp public class TracingMiddleware { // Activity is available when OpenTelemetry is enabled public void Before(object message, Activity? activity, CancellationToken ct) { activity?.SetTag("custom.tag", "value"); } // Exception is NOT available in Before methods public void After(object message, Activity? activity) { activity?.SetTag("result.status", "success"); } // Exception IS available in Finally methods public void Finally(object message, Activity? activity, Exception? ex) { if (ex != null) activity?.SetTag("error", "true"); } } ``` > **Note:** The `Exception?` parameter is only available in `After` and `Finally` methods since exceptions can only occur during or after handler execution. > **Note:** If a middleware's `Before` method is skipped due to short-circuiting by an earlier middleware, that middleware's `Finally` method will not be called. Only middleware whose `Before` has executed will have `Finally` invoked. ## Lifecycle Methods ### Before Runs before the handler. Can return values that are passed to `After` and `Finally`: ::: warning One Method Per Hook Each middleware class may define only **one** `Before` (or `BeforeAsync`) method — the same applies to `After`, `Finally`, and `ExecuteAsync`. Defining multiple overloads of the same hook (e.g., `Before(CreateOrder cmd)` and `Before(UpdateOrder cmd)`) will produce diagnostic error **FMED001** and the middleware will not compile. To handle multiple message types in a single middleware class, accept `object` and use pattern matching: ```csharp public class ValidationMiddleware { public HandlerResult Before(object message) { switch (message) { case CreateOrder create when string.IsNullOrEmpty(create.Name): return HandlerResult.ShortCircuit(Result.Invalid("Name is required")); case UpdateOrder update when update.OrderId == null: return HandlerResult.ShortCircuit(Result.Invalid("OrderId is required")); default: return HandlerResult.Continue(); } } } ``` Alternatively, create separate middleware classes when each message type needs different state (return types from `Before` that flow to `After`/`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); } } } ``` ## Execute Middleware The `ExecuteAsync` method wraps the **entire middleware pipeline** (Before → Handler → After → Finally). This is useful for cross-cutting concerns that need to wrap the full execution, such as: * **Retry logic** - Re-execute the entire pipeline on transient failures * **Circuit breakers** - Fail fast when downstream systems are unavailable * **Timeout handling** - Cancel execution after a deadline ### Execution Flow ```text Execute[1]( Execute[2]( Before[1] → Before[2] → Handler → After[2] → After[1] → Finally[2] → Finally[1] ) ) ``` On each retry, the entire inner pipeline re-executes - all Before middlewares run again, the handler runs again, and all After/Finally middlewares run again. ### Execute Method Signature ```csharp public async ValueTask ExecuteAsync( TMessage message, // The message being handled HandlerExecutionDelegate next, // Delegate to the wrapped pipeline // ... additional DI parameters ) ``` ### Retry Middleware Example Here's a complete retry middleware using [Foundatio Resilience](https://github.com/FoundatioFx/Foundatio). This example demonstrates: * **Custom `[Retry]` attribute** with `[UseMiddleware]` applied to trigger the middleware * **`ExplicitOnly = true`** so the middleware only applies when the attribute is used * **`HandlerExecutionInfo`** to access handler metadata and discover attribute settings ```csharp /// /// Custom attribute that triggers RetryMiddleware and configures retry settings. /// [UseMiddleware(typeof(RetryMiddleware))] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class RetryAttribute : Attribute { public int MaxAttempts { get; set; } = 3; public int DelayMs { get; set; } = 100; public bool UseExponentialBackoff { get; set; } = true; public bool UseJitter { get; set; } = true; } ``` ```csharp /// /// Middleware that wraps the entire pipeline with retry logic. /// Only applies to handlers that use the [Retry] attribute (ExplicitOnly = true). /// [Middleware(Order = 0, ExplicitOnly = true)] // Low order = outermost, ExplicitOnly = only via [Retry] public static class RetryMiddleware { private static readonly IResiliencePolicy DefaultPolicy = new ResiliencePolicyBuilder() .WithMaxAttempts(3) .WithExponentialDelay(TimeSpan.FromMilliseconds(100)) .WithJitter() .Build(); private static readonly ConcurrentDictionary PolicyCache = new(); public static async ValueTask ExecuteAsync( object message, // Takes object - filtering done by [Retry] attribute HandlerExecutionDelegate next, HandlerExecutionInfo handlerInfo, ILogger logger) { var policy = PolicyCache.GetOrAdd(handlerInfo.HandlerMethod, method => { var attr = method.GetCustomAttribute() ?? handlerInfo.HandlerType.GetCustomAttribute(); if (attr == null) return DefaultPolicy; var builder = new ResiliencePolicyBuilder().WithMaxAttempts(attr.MaxAttempts); if (attr.UseExponentialBackoff) builder.WithExponentialDelay(TimeSpan.FromMilliseconds(attr.DelayMs)); else builder.WithLinearDelay(TimeSpan.FromMilliseconds(attr.DelayMs)); if (attr.UseJitter) builder.WithJitter(); return builder.Build(); }); return await policy.ExecuteAsync(async ct => { logger.LogDebug("RetryMiddleware: Attempt for {MessageType}", message.GetType().Name); return await next(); }, default); } } ``` Apply to handlers - the `[Retry]` attribute both **triggers** the middleware AND **configures** it: ```csharp public class OrderHandler { // [Retry] triggers RetryMiddleware with custom settings [Retry(MaxAttempts = 5, DelayMs = 200)] public async Task> HandleAsync(CreateOrder command) { // ... create order with up to 5 retries } // No attribute - RetryMiddleware does NOT run (ExplicitOnly = true) public Result Handle(GetOrder query) { // ... no retry behavior } } ``` ### Key Points * Execute middleware **only supports async** (`ExecuteAsync`) - no sync `Execute` method * The `next` delegate returns `object?` - cast as needed for strongly-typed results * Use low `Order` values (e.g., 0, 1) to wrap as outermost middleware * Execute middleware can access DI parameters like other middleware methods * Use `HandlerExecutionInfo` to access handler type and method for reflection * Cache policies per handler to avoid rebuilding on each invocation > **Complete Example**: See the [Clean Architecture Sample RetryMiddleware](https://github.com/FoundatioFx/Foundatio.Mediator/blob/main/samples/CleanArchitectureSample/src/Common.Module/Middleware/RetryMiddleware.cs), [RetryAttribute](https://github.com/FoundatioFx/Foundatio.Mediator/blob/main/samples/CleanArchitectureSample/src/Common.Module/Middleware/RetryAttribute.cs), and [PaymentHandler](https://github.com/FoundatioFx/Foundatio.Mediator/blob/main/samples/CleanArchitectureSample/src/Orders.Module/Handlers/PaymentHandler.cs) (which randomly throws transient errors to demonstrate retry behavior). ### Caching Middleware The same `[UseMiddleware]` + `ExplicitOnly` pattern works for caching. A `[Cached]` attribute triggers a `CachingMiddleware` that memoizes handler results keyed by message value (C# records have value equality, so identical queries are automatic cache hits). ```csharp [Cached(DurationSeconds = 60)] public async Task> HandleAsync(GetProductCatalog query, ...) { await Task.Delay(500, cancellationToken); // Simulate expensive aggregation // First call is slow; subsequent calls return instantly from cache } ``` > **Complete Example**: See the [CachingMiddleware](https://github.com/FoundatioFx/Foundatio.Mediator/blob/main/samples/CleanArchitectureSample/src/Common.Module/Middleware/CachingMiddleware.cs), [CachedAttribute](https://github.com/FoundatioFx/Foundatio.Mediator/blob/main/samples/CleanArchitectureSample/src/Common.Module/Middleware/CachedAttribute.cs), and [ProductHandler](https://github.com/FoundatioFx/Foundatio.Mediator/blob/main/samples/CleanArchitectureSample/src/Products.Module/Handlers/ProductHandler.cs) in the Clean Architecture Sample. ## 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 HandlerResult.ShortCircuit(Result.Invalid("Validation failed")); 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 `[Middleware]` attribute to control execution order: ```csharp [Middleware(10)] public class ValidationMiddleware { // Runs early in Before, late in After/Finally } [Middleware(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**: * `ExecuteAsync`: Lower order values wrap outermost (run first/last) * `Before`: Lower order values run first * `After`/`Finally`: Higher order values run first (reverse order for proper nesting) ### Relative Ordering Instead of managing numeric order values, you can express ordering relationships between middleware using `OrderBefore` and `OrderAfter`: ```csharp // "I must run before LoggingMiddleware" [Middleware(OrderBefore = [typeof(LoggingMiddleware)])] public class AuthMiddleware { public HandlerResult Before(object message) { // Auth check runs before logging return HandlerResult.Continue(); } } // "I must run after AuthMiddleware" [Middleware(OrderAfter = [typeof(AuthMiddleware)])] public class AuditMiddleware { public void Before(object message) { // Audit runs after auth } } public class LoggingMiddleware { public void Before(object message) { } } ``` The resulting execution order for `Before` is: **AuthMiddleware → LoggingMiddleware → AuditMiddleware** You can specify multiple types in a single declaration: ```csharp // This middleware must run before both Validation and Logging [Middleware(OrderBefore = [typeof(ValidationMiddleware), typeof(LoggingMiddleware)])] public class SecurityMiddleware { public void Before(object message) { } } ``` **How relative ordering works:** * `OrderBefore = [typeof(X)]` means "I run before X" — adds an edge from this middleware to X * `OrderAfter = [typeof(X)]` means "I run after X" — adds an edge from X to this middleware * When no relative constraints exist between two middleware, numeric `Order` is used as a tiebreaker * Middleware with no constraints and no explicit `Order` uses `int.MaxValue` (runs last) * Unknown types in `OrderBefore`/`OrderAfter` are silently ignored (they may not apply to the current handler) ::: warning Circular Dependencies If middleware form a circular dependency (e.g., A says OrderBefore B, and B says OrderBefore A), a compiler warning `FMED012` is emitted and the cycle participants fall back to numeric `Order` sorting. ::: ## 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(); } } ``` ## Handler-Specific Middleware Use the `[UseMiddleware]` attribute to apply middleware to specific handlers rather than globally by message type. ### Basic Usage ```csharp [UseMiddleware(typeof(StopwatchMiddleware))] [UseMiddleware(typeof(AuditMiddleware), Order = 10)] public class OrderHandler { public Result Handle(CreateOrder command) { /* ... */ } } ``` ### Custom Middleware Attributes Create reusable attributes by applying `[UseMiddleware]` to your attribute class: ```csharp [UseMiddleware(typeof(RetryMiddleware))] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class RetryAttribute : Attribute { public int MaxAttempts { get; set; } = 3; } [UseMiddleware(typeof(CachingMiddleware))] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class CachedAttribute : Attribute { public int DurationSeconds { get; set; } = 300; } ``` Usage is clean and expressive: ```csharp public class OrderHandler { [Retry(MaxAttempts = 5)] public async Task> HandleAsync(CreateOrder command) { /* ... */ } [Cached(DurationSeconds = 60)] public Result Handle(GetOrder query) { /* ... */ } } ``` ### ExplicitOnly Middleware Mark middleware with `ExplicitOnly = true` to prevent automatic global application. The middleware will only run when explicitly referenced via `[UseMiddleware]` or a custom attribute: ```csharp // This middleware only runs when a handler uses [Retry] or [UseMiddleware(typeof(RetryMiddleware))] [Middleware(Order = 0, ExplicitOnly = true)] public class RetryMiddleware { public async ValueTask ExecuteAsync(object message, HandlerExecutionDelegate next) { // Retry logic... } } ``` Without `ExplicitOnly = true`, middleware that takes `object` as the message type would run for ALL handlers. ### Method vs Class Level Attributes can be applied at method or class level: ```csharp [Retry] // Applies to all handler methods in this class public class OrderHandler { public Result Handle(CreateOrder command) { /* ... */ } [Cached(DurationSeconds = 120)] // Method-level adds caching for this method only public Result Handle(GetOrder query) { /* ... */ } } ``` ## 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); } } ``` ## 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 Lifetime ### Lifetime Behavior | Lifetime | Behavior | |----------|----------| | **Scoped** | Resolved from DI on every invocation | | **Transient** | Resolved from DI on every invocation | | **Singleton** | Resolved from DI on every invocation (DI handles caching) | | **None/Default** (no constructor deps) | Created once with `new()` and cached | | **None/Default** (with constructor deps) | Created once with `ActivatorUtilities.CreateInstance` and cached | ### Controlling Lifetime with \[Middleware] Attribute Use the `[Middleware]` attribute to explicitly control lifetime: ```csharp // Resolved from DI - DI handles singleton caching [Middleware(Lifetime = MediatorLifetime.Singleton)] public class LoggingMiddleware { /* ... */ } // Resolved from DI on every invocation [Middleware(Lifetime = MediatorLifetime.Scoped)] public class ValidationMiddleware { /* ... */ } // No explicit lifetime - cached in static field since no constructor deps public class SimpleMiddleware { public void Before(object message) { /* ... */ } } ``` ### Project-Level Default Set a default lifetime for all middleware: ```csharp [assembly: MediatorConfiguration(MiddlewareLifetime = MediatorLifetime.Scoped)] ``` ## Middleware Discovery Middleware is automatically discovered by the Foundatio.Mediator source generator. To share middleware across projects: 1. **Create a middleware project** with Foundatio.Mediator package referenced 2. **Reference that project** from your handler projects 3. **Ensure the handler project** also references Foundatio.Mediator The source generator will discover middleware in referenced assemblies that have the Foundatio.Mediator source generator. ### Discovery Rules Middleware classes are found using: 1. **Naming Convention**: Classes ending with `Middleware` (e.g., `LoggingMiddleware`, `ValidationMiddleware`) 2. **Attribute**: Classes marked with `[Middleware]` attribute ### Example: Cross-Assembly Middleware ```text Solution/ ├── Common.Middleware/ # Shared middleware project │ ├── Common.Middleware.csproj # References Foundatio.Mediator │ └── LoggingMiddleware.cs # Discovered by convention └── Orders.Handlers/ # Handler project ├── Orders.Handlers.csproj # References Common.Middleware AND Foundatio.Mediator └── OrderHandler.cs # Uses LoggingMiddleware automatically ``` **Common.Middleware.csproj:** ```xml ``` **Orders.Handlers.csproj:** ```xml ``` **Common.Middleware/LoggingMiddleware.cs:** ```csharp namespace Common.Middleware; // Discovered by naming convention (ends with Middleware) public class LoggingMiddleware { public void Before(object message, ILogger logger) { logger.LogInformation("Handling {MessageType}", message.GetType().Name); } } ``` The middleware will automatically be applied to all handlers in `Orders.Handlers` project. > **💡 Complete Example**: See the [Modular Monolith Sample](https://github.com/FoundatioFx/Foundatio.Mediator/tree/main/samples/CleanArchitectureSample) for a working demonstration of cross-assembly middleware in a multi-module application with shared middleware in `Common.Module` being used by `Products.Module` and `Orders.Module`. ### Setting Middleware Order Control execution order using the `[Middleware(Order = n)]` attribute: ```csharp [Middleware(Order = 1)] // Runs first in Before, last in After/Finally public class LoggingMiddleware { } [Middleware(Order = 10)] // Runs later in Before, earlier in After/Finally public class PerformanceMiddleware { } ``` **Execution flow:** * `Before`: Lower order values run first * `After`/`Finally`: Higher order values run first (reverse order for proper nesting) ## 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) => HandlerResult.ShortCircuit(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 * [Modular Monolith Sample](../../samples/CleanArchitectureSample/) - Complete working example of cross-assembly middleware * [Handler Conventions](./handler-conventions) - Learn handler discovery rules --- --- url: /guide/performance.md --- # Performance Foundatio.Mediator aims to get as close to direct method call performance as possible while providing a full-featured mediator with excellent developer ergonomics. Through C# interceptors and source generators, we eliminate runtime reflection entirely. ## Benchmark Results > 📊 **Last Updated:** 2026-02-23 ### Commands Process a message with no return value. ### Queries Request/response dispatch returning an Order object. ### Events (Publish) Notification dispatched to 2 handlers. ### Full Query (Dependencies + Middleware) Query where handler has an injected service (IOrderService) and timing middleware (Before/Finally or IPipelineBehavior). ### Cascading Messages CreateOrder returns an Order and publishes OrderCreatedEvent to 2 handlers. Foundatio uses tuple returns for automatic cascading; other libraries publish manually. ### Short-Circuit Middleware Middleware returns cached result; handler is never invoked. Each library uses its idiomatic short-circuit approach (IPipelineBehavior, HandlerResult.ShortCircuit, HandlerContinuation.Stop, etc.). ## Running Benchmarks Locally ```bash cd benchmarks/Foundatio.Mediator.Benchmarks dotnet run -c Release ``` --- --- 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 { Success, 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) ``` ### File Results For handlers that return file downloads: ```csharp // From a stream return Result.File(fileStream, "application/pdf", "report.pdf"); // From a byte array return Result.File(bytes, "image/png", "photo.png"); // Inline (no download prompt) — omit the file name return Result.File(stream, "image/jpeg"); ``` `Result.File()` returns a `Result`. The `FileResult` class carries: | Property | Type | Default | Description | | ------------- | ---------- | ----------------------------- | ----------------------------------------------------------------- | | `Stream` | `Stream` | `Stream.Null` | The file content | | `ContentType` | `string` | `"application/octet-stream"` | MIME type | | `FileName` | `string?` | `null` | When set, triggers a `Content-Disposition: attachment` header | When used with [generated endpoints](/guide/endpoints), `Result` is automatically mapped to `Results.File(stream, contentType, fileName)` instead of `Results.Ok()`. ### 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.Message}"); } ``` ### 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.Message}" }; ``` ### Accessing Properties ```csharp public class Result { public bool IsSuccess { get; } public ResultStatus Status { get; } public T Value { get; } // throws if !IsSuccess public T ValueOrDefault { get; } // returns default(T) if !IsSuccess public string Message { get; } public IEnumerable 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(ValidationError.Create("Name", "Name is required")); if (command.Age < 0) errors.Add(ValidationError.Create("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 When using [endpoint generation](/guide/endpoints), `Result` and `Result` are automatically converted to the correct HTTP status codes — no manual mapping needed. See [Result to HTTP Status Mapping](/guide/endpoints#result-to-http-status-mapping) for the default mapping table. To customize the mapping, implement `IMediatorResultMapper` and register it before `AddMediator()`. See [Custom Result Mapping](/guide/endpoints#custom-result-mapping) for details. ### Manual Mapping (Controllers) If you use traditional controllers instead of endpoint generation, you can map results manually: ```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.Message), _ => throw new InvalidOperationException(result.Message) }; ``` ### 4. Compose Results ```csharp public async Task> Handle(GetOrderSummary query) { var orderResult = await _mediator.InvokeAsync>(new GetOrder(query.OrderId)); if (!orderResult.IsSuccess) return Result.FromResult(orderResult); var customerResult = await _mediator.InvokeAsync>(new GetCustomer(orderResult.Value.CustomerId)); if (!customerResult.IsSuccess) return Result.FromResult(customerResult); var summary = new OrderSummary(orderResult.Value, customerResult.Value); return summary; } ``` ## Next Steps * [Handler Conventions](/guide/handler-conventions) - See Result types in handler return values * [Middleware](/guide/middleware) - Middleware patterns including validation * [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 record GetProductStream(string? CategoryId); public class ProductStreamHandler { public async IAsyncEnumerable HandleAsync( GetProductStream query, IProductRepository repository, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var product in repository.GetProductsAsync(query.CategoryId, cancellationToken)) { yield return product; } } } ``` ## Consuming Streaming Results Use `await foreach` to consume a streaming handler's results: ```csharp await foreach (var product in mediator.InvokeAsync>( new GetProductStream("electronics"), cancellationToken)) { Console.WriteLine(product.Name); } ``` You can also use LINQ operators from `System.Linq.Async`: ```csharp await foreach (var product in mediator.InvokeAsync>( new GetProductStream("electronics"), cancellationToken) .Where(p => p.Price > 100) .Take(50)) { await ProcessProductAsync(product); } ``` ## Server-Sent Events (SSE) {#server-sent-events-sse} For real-time push scenarios, set `Streaming = EndpointStreaming.ServerSentEvents` to use ASP.NET Core's built-in `TypedResults.ServerSentEvents()` (requires .NET 10+): ```csharp public record GetEventStream; public record ClientEvent(string EventType, object Data); public class EventStreamHandler(IMediator mediator) { [HandlerEndpoint(Streaming = EndpointStreaming.ServerSentEvents)] public async IAsyncEnumerable Handle( GetEventStream message, [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var evt in mediator.SubscribeAsync( cancellationToken)) { yield return new ClientEvent(evt.GetType().Name, evt); } } } ``` ### Client-Side Consumption SSE endpoints are consumed using the browser's built-in `EventSource` API: ```javascript const source = new EventSource('/api/events'); source.onmessage = (e) => { const data = JSON.parse(e.data); console.log('Received:', data); }; ``` `EventSource` automatically handles reconnection — no WebSocket library needed. ### SSE Configuration Options | Property | Type | Description | | -------------- | ------------------- | --------------------------------------------------------------------- | | `Streaming` | `EndpointStreaming` | `Default` (JSON array) or `ServerSentEvents` (SSE) | | `SseEventType` | `string?` | Optional SSE event type passed to `TypedResults.ServerSentEvents()` | ### When to Use SSE vs Default Streaming | Scenario | Recommended | | ---------------------- | -------------------- | | One-time data export | Default (JSON array) | | Database query results | Default (JSON array) | | Real-time event feed | SSE | | Live notifications | SSE | | Progress updates | SSE | For more on endpoint generation, route conventions, and attribute options, see [Endpoint Generation](./endpoints.md). ## Dynamic Subscriptions with SubscribeAsync Foundatio Mediator supports **dynamic subscriptions** — receive published notifications as an async stream. This is ideal for real-time push scenarios like SSE, where each connected client needs its own live stream of domain events. ### Basic Usage Call `mediator.SubscribeAsync()` to create a subscription that yields every notification assignable to `T`: ```csharp await foreach (var evt in mediator.SubscribeAsync(ct)) { Console.WriteLine($"Order created: {evt.OrderId}"); } ``` The stream continues until the `CancellationToken` is cancelled. Each subscriber gets its own independent buffered channel — no shared state, no coordination required. ### Interface Subscriptions Subscribe to an interface or base class to receive all matching types: ```csharp public interface IDispatchToClient { } public record OrderCreated(string OrderId) : IDispatchToClient; public record ProductUpdated(string ProductId) : IDispatchToClient; // Receives both OrderCreated and ProductUpdated await foreach (var evt in mediator.SubscribeAsync(ct)) { var eventType = evt.GetType().Name; // "OrderCreated" or "ProductUpdated" } ``` Type matching uses `Type.IsAssignableFrom` and is cached — the check only runs once per unique message type, not per subscriber or per publish. ### Combining SubscribeAsync with SSE The most common use case is pushing domain events to browser clients via SSE. Combine `SubscribeAsync` with a streaming handler endpoint: ```csharp public record GetClientEventsStream; public record ClientEvent(string EventType, object Data); public class ClientEventStreamHandler(IMediator mediator) { [HandlerEndpoint(Streaming = EndpointStreaming.ServerSentEvents)] public async IAsyncEnumerable Handle( GetClientEventsStream message, [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var evt in mediator.SubscribeAsync( cancellationToken)) { yield return new ClientEvent(evt.GetType().Name, evt); } } } ``` When any handler publishes a notification that implements `IDispatchToClient`, every connected SSE client receives it automatically. ### Buffer Configuration Each subscriber has a bounded buffer (default: 100 items). When full, the oldest unread item is dropped. You can customize buffer behavior via `SubscriberOptions`: ```csharp await foreach (var evt in mediator.SubscribeAsync( ct, new SubscriberOptions { MaxCapacity = 10 })) { // ... } ``` `SubscriberOptions` also exposes `FullMode` (`BoundedChannelFullMode`) to control what happens when the buffer is full — the default is `DropOldest`. ### Lifecycle * **Subscribe:** `SubscribeAsync()` registers the subscription immediately. * **Receive:** Published notifications matching `T` are written to the subscriber's channel. * **Unsubscribe:** When the `CancellationToken` is cancelled (e.g., SSE client disconnects), the subscription is automatically removed and the channel is completed. * **Dispose:** When `HandlerRegistry` is disposed at app shutdown, all active channels are completed so subscribers exit cleanly. There is zero cost when nobody is subscribed — `PublishAsync` skips the subscription fan-out entirely. --- --- url: /guide/testing.md --- # Testing Because handlers are plain classes with no base types or framework dependencies, testing Foundatio.Mediator applications is straightforward at every level — from isolated unit tests to full HTTP integration tests against generated endpoints. This guide covers three testing tiers: 1. **Unit testing handlers directly** — call handler methods without the mediator 2. **Integration testing with the mediator** — exercise the full pipeline including DI and middleware 3. **Integration testing generated endpoints** — test auto-generated minimal API endpoints over HTTP All examples use [xUnit](https://xunit.net/) and follow standard .NET testing conventions. ## Unit Testing Handlers Directly Handlers are plain classes. You can instantiate them with `new`, call their handle methods directly, and assert the return value — no mediator, no DI container, no framework mocking required. ### Simple Handler Given a handler: ```csharp public record GetGreeting(string Name); public class GreetingHandler { public string Handle(GetGreeting query) => $"Hello, {query.Name}!"; } ``` Test it directly: ```csharp [Fact] public void Handle_ReturnsGreeting() { var handler = new GreetingHandler(); var result = handler.Handle(new GetGreeting("World")); Assert.Equal("Hello, World!", result); } ``` ### Handler with Dependencies When a handler accepts dependencies via constructor injection, pass stubs or mocks directly: ```csharp public record GetOrder(string OrderId); public class OrderHandler(IOrderRepository repository) { public async Task> HandleAsync( GetOrder query, CancellationToken cancellationToken) { var order = await repository.GetByIdAsync(query.OrderId, cancellationToken); return order ?? Result.NotFound($"Order {query.OrderId} not found"); } } ``` ```csharp [Fact] public async Task HandleAsync_WhenOrderExists_ReturnsSuccess() { var expected = new Order("order-1", "customer-1", 99.99m); var repo = new FakeOrderRepository(expected); var handler = new OrderHandler(repo); var result = await handler.HandleAsync( new GetOrder("order-1"), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("order-1", result.Value.Id); } [Fact] public async Task HandleAsync_WhenOrderMissing_ReturnsNotFound() { var repo = new FakeOrderRepository(order: null); var handler = new OrderHandler(repo); var result = await handler.HandleAsync( new GetOrder("missing"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(ResultStatus.NotFound, result.Status); } ``` ::: tip DI Method Parameters Handler methods can also accept additional parameters resolved from DI (like `ILogger`). In unit tests, pass them directly as method arguments: ```csharp public class OrderHandler { public async Task> HandleAsync( CreateOrder command, IOrderRepository repo, // DI-resolved at runtime ILogger logger, // DI-resolved at runtime CancellationToken ct) { // ... } } // In your test: var result = await handler.HandleAsync( new CreateOrder("cust-1", 50m), fakeRepo, NullLogger.Instance, CancellationToken.None); ``` ::: ### Testing Cascading Events Handlers that return tuples produce cascading messages. In a unit test, you can assert the returned tuple directly without the mediator publishing anything: ```csharp [Fact] public async Task HandleAsync_CreateOrder_ReturnsOrderAndEvent() { var repo = new FakeOrderRepository(); var handler = new OrderHandler(repo); var (result, orderCreated) = await handler.HandleAsync( new CreateOrder("cust-1", 100m), NullLogger.Instance, CancellationToken.None); Assert.True(result.IsSuccess); Assert.NotNull(orderCreated); Assert.Equal("cust-1", orderCreated.CustomerId); } ``` ## Integration Testing with the Mediator Integration tests exercise the full pipeline: handler discovery, DI resolution, middleware execution, and dispatch. This verifies that everything wires up correctly. ### Basic Setup Build a real DI container with `AddMediator()` and resolve `IMediator`: ```csharp [Fact] public async Task InvokeAsync_ReturnsExpected() { var services = new ServiceCollection(); services.AddMediator(b => b.AddAssembly()); using var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); var result = await mediator.InvokeAsync(new Ping("Hello")); Assert.Equal("Hello Pong", result); } ``` `AddAssembly()` registers all handlers discovered in the assembly containing `T`. For projects using the default `AddMediator()` call, all referenced assemblies with `[assembly: FoundatioModule]` are auto-discovered. ### Testing Events with Multiple Handlers Use `PublishAsync` to fan out to all registered handlers for a message type: ```csharp public record OrderCreated(string OrderId); public class AuditHandler { public static string? LastOrderId { get; set; } public void Handle(OrderCreated evt) => LastOrderId = evt.OrderId; } public class NotificationHandler { public static bool WasCalled { get; set; } public void Handle(OrderCreated evt) => WasCalled = true; } [Fact] public async Task PublishAsync_InvokesAllHandlers() { AuditHandler.LastOrderId = null; NotificationHandler.WasCalled = false; var services = new ServiceCollection(); services.AddMediator(b => b.AddAssembly()); using var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); await mediator.PublishAsync(new OrderCreated("order-42")); Assert.Equal("order-42", AuditHandler.LastOrderId); Assert.True(NotificationHandler.WasCalled); } ``` ### Testing with Middleware Middleware participates automatically when registered. To test that middleware runs, register it alongside the handler: ```csharp [Middleware(Order = 0)] public class TimingMiddleware { public static bool BeforeCalled { get; set; } public static bool AfterCalled { get; set; } public void Before(object message) => BeforeCalled = true; public void After(object message) => AfterCalled = true; } [Fact] public async Task InvokeAsync_ExecutesMiddlewarePipeline() { TimingMiddleware.BeforeCalled = false; TimingMiddleware.AfterCalled = false; var services = new ServiceCollection(); services.AddMediator(b => b.AddAssembly()); using var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); await mediator.InvokeAsync(new Ping("Test")); Assert.True(TimingMiddleware.BeforeCalled); Assert.True(TimingMiddleware.AfterCalled); } ``` ### Testing Result Types Through the Mediator Assert on `Result` properties when handlers return rich results: ```csharp [Fact] public async Task InvokeAsync_WhenNotFound_ReturnsNotFoundResult() { var services = new ServiceCollection(); services.AddSingleton(new FakeOrderRepository(order: null)); services.AddMediator(b => b.AddAssembly()); using var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); var result = await mediator.InvokeAsync>(new GetOrder("missing")); Assert.False(result.IsSuccess); Assert.Equal(ResultStatus.NotFound, result.Status); } ``` ::: tip Scoped Services If your handlers depend on scoped services, create a scope before resolving the mediator: ```csharp using var provider = services.BuildServiceProvider(); using var scope = provider.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); ``` ::: ### Verifying Unhandled Messages The mediator throws `InvalidOperationException` when no handler is registered for a message: ```csharp [Fact] public async Task InvokeAsync_WithNoHandler_Throws() { var services = new ServiceCollection(); services.AddMediator(); using var provider = services.BuildServiceProvider(); var mediator = provider.GetRequiredService(); await Assert.ThrowsAsync( () => mediator.InvokeAsync(new UnregisteredMessage()).AsTask()); } ``` ## Integration Testing Generated Endpoints When handlers are decorated with endpoint attributes (or discovered automatically), Foundatio.Mediator generates minimal API endpoints. You can test these over HTTP using ASP.NET Core's `WebApplicationFactory`. See [Endpoints](./endpoints) for full details on how routes, HTTP methods, and status codes are generated. ### Project Setup Add the test infrastructure packages to your test project: ```xml ``` Ensure your API project exposes its entry point for testing. If using top-level statements, add to your API project: ```csharp // At the bottom of Program.cs (or in a partial class) public partial class Program { } ``` ### Basic Endpoint Test Given a handler that generates a `GET /api/orders/{orderId}` endpoint: ```csharp [HandlerEndpointGroup("Orders")] public class OrderHandler(IOrderRepository repository) { [HandlerAllowAnonymous] public async Task> HandleAsync( GetOrder query, CancellationToken cancellationToken) { var order = await repository.GetByIdAsync(query.OrderId, cancellationToken); return order ?? Result.NotFound($"Order {query.OrderId} not found"); } } ``` Test the generated endpoint: ```csharp public class OrderEndpointTests : IClassFixture> { private readonly HttpClient _client; public OrderEndpointTests(WebApplicationFactory factory) { _client = factory.WithWebApplicationBuilder(builder => { builder.Services.AddSingleton( new FakeOrderRepository( new Order("order-1", "cust-1", 99.99m))); }).CreateClient(); } [Fact] public async Task GetOrder_ReturnsOrder() { var response = await _client.GetAsync("/api/orders/order-1"); response.EnsureSuccessStatusCode(); var order = await response.Content .ReadFromJsonAsync(); Assert.NotNull(order); Assert.Equal("order-1", order.Id); } [Fact] public async Task GetOrder_WhenMissing_Returns404() { var response = await _client.GetAsync("/api/orders/does-not-exist"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } ``` ### Testing POST Endpoints For handlers that create resources, the generated endpoint accepts a JSON body: ```csharp [Fact] public async Task CreateOrder_Returns201() { var response = await _client.PostAsJsonAsync("/api/orders", new { CustomerId = "cust-1", Amount = 50.00m, Description = "Test" }); Assert.Equal(HttpStatusCode.Created, response.StatusCode); var order = await response.Content.ReadFromJsonAsync(); Assert.NotNull(order); Assert.Equal("cust-1", order.CustomerId); } ``` ### Testing with Authentication When endpoints require authorization, configure a test authentication scheme: ```csharp public class AuthOrderEndpointTests : IClassFixture> { private readonly WebApplicationFactory _factory; public AuthOrderEndpointTests(WebApplicationFactory factory) { _factory = factory; } private HttpClient CreateAuthenticatedClient(string role = "User") { return _factory.WithWebApplicationBuilder(builder => { builder.Services.AddAuthentication("Test") .AddScheme( "Test", options => { }); builder.Services.AddAuthorization(); builder.Services.Configure(o => o.Role = role); }).CreateClient(); } [Fact] public async Task CreateOrder_WithoutAuth_Returns401() { var client = _factory.CreateClient(); var response = await client.PostAsJsonAsync("/api/orders", new { CustomerId = "cust-1", Amount = 50m }); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task CreateOrder_WithUserRole_Succeeds() { var client = CreateAuthenticatedClient("User"); var response = await client.PostAsJsonAsync("/api/orders", new { CustomerId = "cust-1", Amount = 50m }); Assert.True(response.IsSuccessStatusCode); } } ``` ::: tip Result-to-HTTP Status Mapping Foundatio.Mediator automatically maps `Result` statuses to HTTP responses: | Result Status | HTTP Status | | --- | --- | | `Success` | 200 OK | | `Created` | 201 Created | | `NoContent` | 204 No Content | | `BadRequest` | 400 Bad Request | | `Unauthorized` | 401 Unauthorized | | `Forbidden` | 403 Forbidden | | `NotFound` | 404 Not Found | | `Invalid` | 422 Unprocessable Entity | | `Error` | 500 Internal Server Error | See [Result Types](./result-types) for details on the `Result` pattern. ::: ### Swapping Services for Testing Use `WithWebApplicationBuilder` to replace real services with test doubles: ```csharp var client = factory.WithWebApplicationBuilder(builder => { builder.Services.AddSingleton(new FakeOrderRepository()); builder.Services.AddSingleton(new FakePaymentService()); }).CreateClient(); ``` This lets you control handler behavior without changing any handler code — the same DI injection that powers production code makes test doubles drop in seamlessly. ## Summary | Testing Tier | What It Tests | Setup Complexity | Speed | | --- | --- | --- | --- | | **Unit tests** | Handler logic in isolation | None — `new` up the handler | Fastest | | **Mediator integration** | DI wiring, middleware, dispatch | `ServiceCollection` + `AddMediator()` | Fast | | **Endpoint integration** | HTTP routing, serialization, auth | `WebApplicationFactory` | Moderate | Start with unit tests for business logic, add mediator integration tests for pipeline behavior, and use endpoint tests to verify routing and HTTP semantics. Because handlers are plain classes throughout, each tier builds naturally on the previous one. --- --- url: /guide/troubleshooting.md --- # Troubleshooting This guide covers common issues and debugging techniques when working with Foundatio Mediator. ## IntelliSense Shows Errors Before First Build Since Foundatio Mediator relies on source generators, extension methods like `Map{X}Endpoints()`, handler registration helpers, and interceptor attributes don't exist until the generator runs during a build. This means: * **Red squiggles** will appear on calls to generated methods before you build. * **IntelliSense / autocomplete** won't suggest generated methods until after the first build. **Solution:** Run `dotnet build` (or build from your IDE) once after adding the package or changing configuration. The generated code will be picked up automatically. Subsequent edits trigger incremental generation so the squiggles will resolve on their own. ::: tip If IntelliSense still shows errors after building, restart the IDE language server. In VS Code: `Ctrl+Shift+P` → "Restart Language Server". In Visual Studio: close and reopen the solution. ::: ## Viewing Generated Source Code Since Foundatio Mediator uses source generators, it can be helpful to see the actual code being generated. This is useful for: * Understanding how handlers are dispatched * Debugging unexpected behavior * Verifying interceptor generation * Learning how the mediator works internally ### Enabling Generated File Output Add the following to your `.csproj` file to emit generated files to disk: ```xml Generated true ``` After building your project, you'll find the generated files in the `Generated` folder: ``` Generated/ Foundatio.Mediator/ Foundatio.Mediator.MediatorGenerator/ YourHandler_YourMessage_Handler.g.cs YourProject_MediatorHandlers.g.cs InterceptsLocationAttribute.g.cs ... ``` ### Understanding Generated Files | File Pattern | Description | |-------------|-------------| | `*_Handler.g.cs` | Handler wrapper with strongly-typed dispatch and middleware pipeline | | `*_MediatorHandlers.g.cs` | DI registration code for all handlers | | `InterceptsLocationAttribute.g.cs` | Interceptor attribute for compile-time call redirection | | `*_FoundatioModuleAttribute.g.cs` | Module marker for cross-assembly handler discovery | ### Viewing All Registered Handlers The quickest way to see every discovered handler at startup is to enable `LogHandlers`: ```csharp // Option 1: via MediatorOptions services.AddMediator(new MediatorOptions { LogHandlers = true }); // Option 2: via builder services.AddMediator(b => b.LogHandlers()); ``` This prints aligned, columnar output to the console during `AddMediator()`: ```text Foundatio.Mediator registered 5 handler(s): CreateOrder → OrderHandler.HandleAsync Result [async] GetOrder → OrderHandler.Handle Result GetUser → UserHandler.HandleAsync Result [async] OrderCreated → EventHandler.HandleAsync [async] UserCreated → EventHandler.Handle ``` You can also call `ShowRegisteredHandlers()` manually after building the service provider, optionally passing an `ILogger`: ```csharp var registry = serviceProvider.GetRequiredService(); registry.ShowRegisteredHandlers(logger); // uses ILogger registry.ShowRegisteredHandlers(); // falls back to Console.WriteLine ``` Each `HandlerRegistration` also exposes diagnostic metadata for programmatic inspection: ```csharp foreach (var reg in registry.Registrations) { Console.WriteLine($"{reg.SourceHandlerName}.{reg.MethodName}({GetShortName(reg.MessageTypeName)}) → {reg.ReturnTypeName}"); } ``` **If a handler is missing from this list:** * Verify the class name ends with `Handler` or `Consumer` * Check that the method name is `Handle`, `HandleAsync`, `Consume`, or `ConsumeAsync` * Ensure the handler isn't marked with `[FoundatioIgnore]` * Handlers nested in generic classes are not supported (e.g., `OuterClass.MyHandler`) * Verify `AddHandlers()` was called during DI configuration For deeper inspection, you can also [view the generated source files](#enabling-generated-file-output) to see the actual registration code in `*_MediatorHandlers.g.cs`. ### Viewing the Middleware Pipeline Enable `LogMiddleware` to dump the middleware pipeline in execution order at startup: ```csharp // Option 1: via MediatorOptions services.AddMediator(new MediatorOptions { LogMiddleware = true }); // Option 2: via builder services.AddMediator(b => b.LogMiddleware()); // Option 3: enable both handlers and middleware services.AddMediator(b => b.LogHandlers().LogMiddleware()); ``` This prints the middleware pipeline sorted by execution order: ```text Foundatio.Mediator middleware pipeline (3): 1. AuthMiddleware [Before] Order: 5 2. TimingMiddleware [Before, After, Finally] Order: 10 3. AuditMiddleware [Execute] [explicit-only] ``` Each line shows: * **Name** — the middleware class name * **Hooks** — which hooks the middleware implements (`Before`, `After`, `Finally`, `Execute`) * **Order** — numeric execution order (lower runs first in `Before`, reverse in `After`/`Finally`) * **Scope** — if the middleware targets a specific message type (e.g., ``), it's shown; global middleware (`object`) is omitted for clarity * **Flags** — `static` if the middleware uses static methods, `explicit-only` if it requires `[UseMiddleware]` You can also call `ShowRegisteredMiddleware()` manually: ```csharp var registry = serviceProvider.GetRequiredService(); registry.ShowRegisteredMiddleware(logger); // uses ILogger registry.ShowRegisteredMiddleware(); // falls back to Console.WriteLine ``` ### Example Generated Handler Here's what a generated handler wrapper looks like: ```csharp internal static class OrderHandler_CreateOrder_Handler { public static async Task> HandleAsync( IServiceProvider serviceProvider, CreateOrder message, CancellationToken cancellationToken) { var logger = serviceProvider.GetService()?.CreateLogger("OrderHandler"); // OpenTelemetry activity using var activity = MediatorActivitySource.Instance.StartActivity("CreateOrder"); // Middleware pipeline var validationMiddleware = GetMiddleware(serviceProvider); validationMiddleware.Before(message); // Handler invocation var handlerInstance = GetOrCreateHandler(serviceProvider); var result = await handlerInstance.HandleAsync(message, cancellationToken); return result; } } ``` ## Common Issues ### Event Handlers Not Being Called **Symptom:** Event handlers (notification handlers) are not being invoked when events are published. **Cause:** Handler discovery is scoped to the current project and its referenced assemblies. This is by design for performance - the source generator only generates dispatch code for handlers it can see at compile time. **Key Points:** * Handlers are only discovered in the **current project** and **directly referenced projects** * If Project A publishes an event, only handlers in Project A or projects that A references will be called * Handlers in projects that reference Project A (downstream) will NOT be discovered **Example:** ``` Common.Module (defines events + cross-cutting handlers) ↑ Orders.Module (references Common, publishes OrderCreated) ↑ Api (references Orders) ``` In this structure: * When `Orders.Module` publishes `OrderCreated`, handlers in `Common.Module` ARE called (referenced) * Handlers defined in `Api` are NOT called (Api references Orders, not the other way around) **Solutions:** 1. **Move shared event handlers to a common/lower-level module** that all publishing modules reference 2. **Define events in the common module** so all handlers can subscribe to the same event type 3. Use `AddAssembly()` in your mediator configuration to register handlers from specific assemblies at runtime ```csharp // In Program.cs, register assemblies containing handlers builder.Services.AddMediator(c => { c.AddAssembly(); // Common.Module (events + handlers) c.AddAssembly(); // Reports.Module }); ``` **Why this design?** The source generator analyzes handlers at compile time to generate optimized, direct dispatch code. This eliminates runtime reflection and provides maximum performance. The trade-off is that handler discovery follows project reference boundaries. ### Handler Not Found **Symptom:** `InvalidOperationException: No handler found for message type X` **Causes:** 1. Handler class doesn't follow naming conventions (must end in `Handler` or `Consumer`) 2. Handler method doesn't follow naming conventions (`Handle`, `HandleAsync`, `Consume`, `ConsumeAsync`) 3. Handler is in a different assembly and not registered 4. Missing call to `AddHandlers()` in DI configuration 5. Handler is nested inside a generic class (not supported) **Debugging:** Use the [`HandlerRegistry`](#viewing-all-registered-handlers) to see which handlers are currently registered at runtime. **Solutions:** ```csharp // Ensure handlers are registered services.AddMediator(); YourProject_MediatorHandlers.AddHandlers(services); // Or use the [Handler] attribute for non-conventional names [Handler] public class MyCustomProcessor { public void Process(MyMessage msg) { } } ``` ### Scoped Services Returning Same Instance **Symptom:** Scoped services (like `DbContext`) return the same instance across different HTTP requests or DI scopes, causing stale data, disposed context errors, or cross-request data leakage. **Cause:** The mediator is registered as Singleton and captures the root `IServiceProvider` at construction. All service resolution uses this root provider, making scoped services behave like singletons. > **Note:** This should not happen with the default configuration, because ASP.NET Core apps auto-detect as Scoped. This issue only occurs if you explicitly forced `ServiceLifetime.Singleton`. **Solution:** Remove the explicit Singleton override, or switch to Scoped: ```csharp // Let auto-detection pick the right lifetime (Scoped for ASP.NET Core) services.AddMediator(); // Or explicitly set Scoped services.AddMediator(b => b.SetMediatorLifetime(ServiceLifetime.Scoped)); ``` This ensures each DI scope gets its own mediator that resolves services from the correct scope. **See also:** [Mediator Lifetime and Scoped Services](./dependency-injection.md#mediator-lifetime-and-scoped-services) ### Multiple Handlers Found **Symptom:** `InvalidOperationException: Multiple handlers found for message type X` **Cause:** More than one handler exists for the same message type when using `Invoke`/`InvokeAsync`. **Solution:** Use `PublishAsync` for messages that should be handled by multiple handlers, or remove duplicate handlers. ### Sync Handler with Async Call Site **Symptom:** Compilation works but you want to understand the async wrapping. When you call `InvokeAsync` on a synchronous handler, the generated code wraps the result: ```csharp // Your sync handler public string Handle(GetGreeting msg) => $"Hello, {msg.Name}!"; // Generated interceptor for InvokeAsync public static ValueTask InterceptInvokeAsync(...) { // Wraps sync result in ValueTask return new ValueTask(Handle(...)); } ``` ### Sync Invoke on Tuple-Returning Handler **Symptom:** Compilation error `FMED010` **Cause:** Handlers that return tuples (for cascading messages) cannot use synchronous `Invoke` because cascading messages must be published asynchronously. **Solution:** Use `InvokeAsync` instead: ```csharp // Error: Can't use sync Invoke with tuple return var order = mediator.Invoke(new CreateOrder(...)); // Correct: Use InvokeAsync var order = await mediator.InvokeAsync(new CreateOrder(...)); ``` ### Interceptors Not Working **Symptom:** Calls go through DI lookup instead of direct dispatch. **Causes:** 1. Interceptors disabled via `DisableInterceptors = true` in `[assembly: MediatorConfiguration]` 2. Cross-assembly calls (interceptors only work within the same assembly) 3. C# language version below 11 **Solutions:** ```csharp // Ensure interceptors are enabled (default) [assembly: MediatorConfiguration(DisableInterceptors = false)] ``` ```xml preview ``` ### Middleware Not Executing **Symptom:** Middleware `Before`/`After`/`Finally`/`ExecuteAsync` methods not being called. **Causes:** 1. Middleware class doesn't end in `Middleware` 2. Middleware is in a different assembly than handlers 3. Method signatures don't match expected patterns **Solution:** Ensure middleware follows conventions: ```csharp // Class must end in "Middleware" public class LoggingMiddleware { // First parameter must be the message type (or object for all messages) public void Before(object message) { } public void After(object message) { } public void Finally(object message, Exception? ex) { } } ``` ## Diagnostic Codes | Code | Severity | Description | |------|----------|-------------| | `FMED008` | Error | Synchronous invoke on asynchronous handler | | `FMED009` | Error | Synchronous invoke on handler with async middleware | | `FMED010` | Error | Synchronous invoke on handler with tuple return | | `FMED011` | Error | Multiple Execute methods in a middleware class. Only one Execute/ExecuteAsync method is allowed per middleware class. | | `FMED012` | Warning | Circular ordering dependency detected between middleware or handlers using `OrderBefore`/`OrderAfter`. Falls back to numeric `Order`. | | `FMED014` | Warning | GET/DELETE endpoint message type has no parameterless constructor and no route/query parameters. | | `FMED015` | Warning | `HandlerEndpointGroup` `RoutePrefix` starts with the global `EndpointRoutePrefix`, producing a doubled path (e.g. `/api/api/...`). | | `FMED017` | Info | Shows the computed endpoint route on a handler method (e.g. `Endpoint: GET /api/products/{productId}`). Provides a "Make endpoint explicit" code fix to freeze the route into an explicit attribute. | | `FMED018` | Info | Shows the handler location on a message type declaration (e.g. `Handled by: ProductHandler`). | ## Getting Help If you encounter issues not covered here: 1. Check the [generated source code](#viewing-generated-source-code) to understand what's happening 2. Review the [GitHub repository](https://github.com/FoundatioFx/Foundatio.Mediator) for existing issues 3. Open a new issue with: * Your handler and message code * The generated wrapper code (from `Generated` folder) * The full error message or unexpected behavior --- --- url: /guide/what-is-foundatio-mediator.md --- # What is Foundatio Mediator? Foundatio Mediator is a convention-based mediator library for .NET that makes it easy to build loosely coupled, maintainable, and testable applications — without sacrificing performance or drowning in boilerplate. It leverages source generators and C# interceptors to deliver near-direct call performance at runtime while giving your team clean architectural boundaries at design time. ## The Problem: How Codebases Become Unmaintainable Every application starts clean. A controller calls a service, the service calls a repository, and everything is easy to follow. But as the application grows, things get tangled: * **Services call other services**, creating a web of direct dependencies that's hard to trace and harder to change * **Business logic spreads** across controllers, services, and helpers with no clear boundaries * **Testing becomes painful** — to test one class, you need to mock a chain of dependencies it was never designed to work without * **Changes ripple unpredictably** — a small modification in one service breaks three others that depend on it directly * **New team members struggle** to understand what calls what and where a feature actually lives This is the **big ball of mud** — and it's the natural outcome when components communicate directly instead of through clear, well-defined boundaries. ## The Mediator Pattern: A Way Out The mediator pattern solves this by introducing a simple rule: **components never call each other directly**. Instead, they send messages through a central mediator, and handlers pick them up on the other side. ```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] ``` This single change has profound effects: * **Loose coupling** — The sender doesn't know (or care) who handles the message. You can change, replace, or remove handlers without touching callers. * **Compose with events** — Publish an event like `OrderCreated` and any number of handlers react — sending emails, updating inventory, writing audit logs — without knowing about each other. Add new behavior without modifying existing code. * **Clear boundaries** — Each handler does one thing. Business logic has an obvious home. * **Easy testing** — Handlers are self-contained. Test them in isolation with real assertions, not mock verification chains. * **Safe refactoring** — Rename, split, or reorganize handlers without breaking the rest of the app. The catch? Traditional mediator libraries make you pay for these benefits with boilerplate interfaces, runtime reflection overhead, and framework lock-in. Foundatio Mediator eliminates those costs. ## 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) { /* ... */ } } ``` Unlike traditional mediator libraries that lock you into rigid interface contracts, conventions give you **unprecedented flexibility**: * **Sync or async** - Return `void`, `Task`, `T`, `Task`, `ValueTask` * **Any parameters** - Message first, then any dependencies injected automatically * **Multiple handlers per class** - Group related operations naturally * **Static handlers** - Zero allocation for stateless operations * **Tuple returns** - Cascading messages for event-driven workflows ```csharp // All of these are valid handlers: public int Handle(AddNumbers q) => q.A + q.B; // Sync, returns value public void Handle(LogMessage cmd) => _log.Info(cmd.Text); // Fire-and-forget public async Task HandleAsync(GetUser q, IRepo r) => ...; // Async with DI public (Order, OrderCreated) Handle(CreateOrder c) => ...; // Cascading events ``` ### 🔧 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/Execute hooks: ```csharp public class ValidationMiddleware { public HandlerResult Before(object message) { if (!IsValid(message)) return HandlerResult.ShortCircuit(Result.Invalid("Validation failed")); 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); } } ``` ## What This Means for Your Team The features above aren't just technical checkboxes — they translate directly into a healthier codebase and a more productive team: * **Avoid the big ball of mud** — Message-based communication enforces boundaries that prevent the tight coupling that makes large codebases unmaintainable. Your code stays organized as it grows. * **Compose logic through events** — When an order is created, the email handler, audit handler, and inventory handler all react independently. None of them know about each other. Need a new reaction? Add a handler — no existing code changes. * **Ship changes confidently** — When handlers are self-contained and loosely coupled, you can modify one feature without worrying about breaking others. Refactoring goes from scary to routine. * **Test without fighting the framework** — Handlers are plain classes. Write focused unit tests that assert on real behavior, not mock setups. No mediator fakes, no DI container in tests. * **Onboard developers faster** — Clear conventions mean new team members learn the pattern once and can navigate the entire codebase. Every feature follows the same structure: message in, handler processes, result out. * **No performance penalty for good architecture** — Unlike other mediator libraries that add measurable overhead per call, Foundatio Mediator compiles away the indirection. You get a well-structured codebase that runs as fast as hand-wired code. * **No boilerplate tax** — No marker interfaces, no base classes, no manual DI registration. The source generator handles the wiring so you can focus on business logic. ## When to Use Foundatio Mediator ### ✅ Great For * **Any app that needs to stay maintainable** as it grows beyond a handful of services * **Clean Architecture** applications with command/query separation * **Microservices** with clear request/response boundaries * **Event-driven** architectures with publish/subscribe patterns * **Large teams** needing consistent patterns and conventions * **High-performance** scenarios where mediator overhead is usually accepted as a cost of good architecture ### ⚠️ 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 > **Note:** If you prefer explicit interfaces over conventions, Foundatio Mediator fully supports that too! Use `IHandler` marker interface or `[Handler]` attributes, and optionally disable conventional discovery. See [Handler Conventions](./handler-conventions#explicit-handler-declaration) for details. ## 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 * [Samples](../samples/) - See practical implementations --- --- url: /guide/why-foundatio-mediator.md --- # Why Choose Foundatio Mediator? ## The Problem: Complexity That Creeps In Every .NET application starts clean. But as features accumulate, services start calling other services, dependencies multiply, and before long you have a tightly-coupled codebase that’s hard to test, hard to change, and hard for new team members to navigate. This is the **big ball of mud**, and it’s the default outcome when there’s nothing enforcing boundaries between components. The mediator pattern solves this: route all interactions through messages instead of direct calls, and your components stay decoupled by design. Need something to happen when an order is created? Publish an `OrderCreated` event — email, audit, and inventory handlers each react independently, without knowing about each other. Adding new behavior means adding a handler, not modifying existing code. But traditional mediator libraries make you pay for this architecture with boilerplate interfaces, runtime reflection overhead, and framework lock-in. **Foundatio Mediator gives you the architecture benefits without the costs.** ## What You Actually Get | Benefit | How | | ------- | --- | | **Loose coupling without ceremony** | No interfaces, no base classes — just naming conventions. Components communicate through messages, never directly. | | **Compose logic through events** | Publish an event and any number of handlers react independently. Add new behavior without touching existing code — the ultimate open/closed principle. | | **Testability by default** | Handlers are plain classes. Unit test them like any other object — no mediator mocking, no DI container setup. | | **Maintainability at scale** | Clear command/query/event boundaries prevent your codebase from growing into an unmaintainable mess. Every feature follows the same pattern. | | **No performance penalty** | Source generators and C# interceptors compile mediator calls into near-direct method calls. The pattern pays for itself. | | **No boilerplate tax** | The source generator handles all DI registration, dispatch wiring, and endpoint generation. You write business logic, nothing else. | | **Faster onboarding** | New developers learn the conventions once and can navigate the entire codebase. Message in, handler processes, result out. | ## 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: * **Significantly faster** than other mediator libraries * **Zero allocations** for fire-and-forget commands * **No reflection overhead** at runtime * **Full compiler optimizations** including inlining For detailed benchmark comparisons, see the [Performance](./performance.md) page which is kept up-to-date with the latest results. ## 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); } ``` ### But Isn't Convention-Based Discovery "Too Magic"? Some developers prefer explicit interfaces because they feel conventions are "too magical." Let's address this concern directly. **All abstractions are "magic."** Consider what you already accept without question: * **ASP.NET Controllers** - Classes ending in `Controller` are automatically discovered and routed * **Entity Framework** - Properties named `Id` become primary keys; navigation properties create relationships * **xUnit/NUnit** - Methods with `[Fact]` or `[Test]` attributes are discovered and executed * **Dependency Injection** - Constructor parameters are magically resolved from a container * **C# Records** - `record Person(string Name)` generates equality, `ToString()`, and deconstruction The difference between "magic" and "convention" is simply **familiarity**. Once you learn a convention, it becomes invisible—you stop thinking about it. **Why conventions are actually *better* for mediator patterns:** | Traditional Interfaces | Convention-Based | | ---------------------- | ---------------- | | Forces `IRequestHandler` inheritance | Plain classes with any name ending in `Handler` | | One handler method per class (or awkward multiple interface inheritance) | Multiple handler methods per class, naturally grouped | | Fixed method signature: `Handle(TRequest, CancellationToken)` | Flexible signatures: sync/async, any parameters via DI | | Must return `Task` even for sync operations | Return `void`, `T`, `Task`, `Task`, `ValueTask`, `ValueTask`, `Result`, tuples | | Handler classes must be registered in DI | Auto-discovered at compile time | | Runtime dispatch via reflection | Compile-time interceptors for near-direct call performance | **With conventions, you get unprecedented flexibility:** ```csharp // Sync handler - no async overhead public int Handle(AddNumbers query) => query.A + query.B; // Async handler with injected dependencies public async Task HandleAsync(GetUser query, IUserRepo repo, CancellationToken ct) => await repo.FindAsync(query.Id, ct); // Static handler - zero allocation, maximum performance public static class MathHandler { public static int Handle(Multiply query) => query.A * query.B; } // Multiple handlers in one cohesive class public class OrderHandler { public Result Handle(CreateOrder cmd) { /* ... */ } public Result Handle(GetOrder query) { /* ... */ } public Result Handle(DeleteOrder cmd) { /* ... */ } } // Cascading messages with tuple returns public (User user, UserCreated evt) Handle(CreateUser cmd) { var user = CreateUser(cmd); return (user, new UserCreated(user.Id)); // Event auto-published! } ``` **If you still prefer explicit declaration**, that's supported too: ```csharp // Use IHandler marker interface public class MyService : IHandler { public User Handle(GetUser query) { /* ... */ } } // Or use [Handler] attribute on class or method [Handler] public class AnotherService { public void Process(SendEmail cmd) { /* ... */ } } // Disable conventions entirely via assembly attribute // [assembly: MediatorConfiguration(HandlerDiscovery = HandlerDiscovery.Explicit)] ``` ### 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 [Middleware(Order = 10)] public class ValidationMiddleware { public HandlerResult Before(object message) { if (!TryValidate(message, out var errors)) return HandlerResult.ShortCircuit(Result.Invalid(errors)); return HandlerResult.Continue(); } } [Middleware(Order = 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 * **Any app that needs to stay maintainable** as it grows — the mediator pattern prevents the big ball of mud * **Clean architecture** implementations with CQRS patterns * **Event-driven systems** with publish/subscribe needs * **Teams that value consistency** — conventions mean everyone follows the same patterns * **High-performance applications** where other mediator libraries add unacceptable overhead * **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 * **Existing MediatR codebases** where migration cost isn't justified > **Note:** If your team prefers explicit interfaces over conventions, Foundatio Mediator supports that too! Use `IHandler` or `[Handler]` attributes and optionally disable conventional discovery entirely. See [Explicit Handler Declaration](./handler-conventions#explicit-handler-declaration). ## 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 ## Getting Started Ready to experience the benefits of Foundatio Mediator? 1. [Installation & Setup](./getting-started) - Get running in minutes 2. [Simple Examples](https://github.com/FoundatioFx/Foundatio.Mediator/tree/main/samples) - See the patterns in action The combination of loose coupling, testability, maintainability, and exceptional performance — without the usual boilerplate tax — makes Foundatio Mediator an excellent choice for modern .NET applications.