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
dotnet add package Foundatio.Mediator2. Define a message and handler
// 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
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMediator();
var app = builder.Build();
app.MapGet("/ping", (IMediator mediator) =>
mediator.Invoke<string>(new Ping("Hello")));
app.Run();var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMediator();
var host = builder.Build();
var mediator = host.Services.GetRequiredService<IMediator>();
var result = mediator.Invoke<string>(new Ping("Hello"));
Console.WriteLine(result); // Pong: HelloThat's it. No interfaces, no base classes, no registration — the source generator handles everything at compile time with near-direct-call performance.
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 for the full list.
Async Handlers
Handlers can be async and accept additional parameters resolved from DI:
public record GetUser(int Id);
public class UserHandler
{
public async Task<User> HandleAsync(GetUser query, IUserRepository repo, CancellationToken ct)
{
return await repo.GetByIdAsync(query.Id, ct);
}
}var user = await mediator.InvokeAsync<User>(new GetUser(42));The first parameter is always the message. Everything else — services, CancellationToken — is injected automatically. See Handler Conventions for the full set of discovery rules, method names, and signature options.
Generate API Endpoints
Endpoints are generated automatically for all handlers:
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");
}// 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 for route customization, naming conventions, and more.
Result Types
Return Result<T> instead of throwing exceptions for expected failures:
public class TodoHandler
{
public Result<Todo> 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<Todo>
}
}When used with generated endpoints, Result<T> maps automatically to the correct HTTP status code — 200, 404, 400, 409, etc. See Result Types for the full API.
Events
Publish messages to multiple handlers with PublishAsync:
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<AuditHandler> logger)
=> logger.LogInformation("Order {OrderId} created at {Time}", e.OrderId, e.CreatedAt);
}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 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:
public class EventStreamHandler(IMediator mediator)
{
[HandlerEndpoint(Streaming = EndpointStreaming.ServerSentEvents)]
public async IAsyncEnumerable<object> Handle(GetEventStream message, [EnumeratorCancellation] CancellationToken ct)
{
await foreach (var evt in mediator.SubscribeAsync<INotification>(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 for the full API.
Middleware
Add cross-cutting concerns by creating classes ending in Middleware:
public class LoggingMiddleware
{
public void Before(object message, ILogger<LoggingMiddleware> logger)
=> logger.LogInformation("→ {MessageType}", message.GetType().Name);
public void After(object message, ILogger<LoggingMiddleware> logger)
=> logger.LogInformation("← {MessageType}", message.GetType().Name);
}Middleware supports Before, After, Finally, and ExecuteAsync hooks with state passing, ordering, and short-circuiting. See Middleware for the full pipeline.
Cross-Assembly Handlers
In multi-project solutions, register assemblies so handlers in referenced projects are discovered:
builder.Services.AddMediator(c => c
.AddAssembly<OrderCreated>() // Orders.Module
.AddAssembly<CreateProduct>() // Products.Module
);See Clean Architecture for a complete modular monolith example.
Next Steps
| Topic | Description |
|---|---|
| Handler Conventions | All discovery rules, method names, static handlers, explicit attributes |
| Events & Notifications | Publish/subscribe, cascading messages, dynamic subscriptions |
| Authorization | Built-in auth for endpoints and direct mediator calls, policies, roles |
| Dependency Injection | Lifetimes, parameter injection, constructor vs method injection |
| Result Types | Result<T> API, status codes, validation errors |
| Middleware | Pipeline hooks, ordering, state passing, short-circuiting |
| Endpoints | Route conventions, OpenAPI, authorization, filters |
| Configuration | All compile-time and runtime options |
| Streaming Handlers | IAsyncEnumerable<T> support and dynamic subscriptions |
| Performance | Benchmarks and how interceptors work |
| Clean Architecture | Modular monolith example with cross-assembly handlers |
| Troubleshooting | Common issues and solutions |
LLM-Friendly Docs
For AI assistants, we provide llms.txt and llms-full.txt following the llmstxt.org standard.