# Comparison
import { Callout } from 'fumadocs-ui/components/callout';
This page compares TurboMediator against the two most referenced .NET libraries in this space: **MediatR** (the original mediator library) and **MassTransit** (the distributed messaging framework).
These libraries solve different problems. MassTransit is a distributed message bus — comparing it here is useful because teams sometimes reach for it when they only need in-process messaging. MediatR is the closest direct competitor.
***
Feature Matrix [#feature-matrix]
| Feature | MediatR | MassTransit | TurboMediator |
| ----------------------------------------- | :-------: | :---------: | :-----------: |
| **Core** | | | |
| Request / Response | ✅ | ✅ | ✅ |
| Notifications / Events | ✅ | ✅ | ✅ |
| Streaming | ✅ | ✅ | ✅ |
| Pipeline behaviors | ✅ | ✅ | ✅ |
| **Performance** | | | |
| Source-generated dispatch (no reflection) | ❌ | ❌ | ✅ |
| Native AOT support | ❌ | Partial | ✅ |
| Compile-time missing handler detection | ❌ | ❌ | ✅ |
| Zero-overhead in-process dispatch | ❌ | ❌ | ✅ |
| **Cross-cutting Concerns** | | | |
| Built-in caching | ❌ | ❌ | ✅ |
| Built-in validation | ❌ | ❌ | ✅ |
| FluentValidation integration | Community | ❌ | ✅ |
| Rate limiting | ❌ | ❌ | ✅ |
| Distributed locking | ❌ | ❌ | ✅ |
| Feature flags | ❌ | ❌ | ✅ |
| **Resilience** | | | |
| Retry | ❌ | ✅ | ✅ |
| Circuit breaker | ❌ | ✅ | ✅ |
| Timeout | ❌ | ✅ | ✅ |
| Fallback | ❌ | ❌ | ✅ |
| Hedging | ❌ | ❌ | ✅ |
| **Observability** | | | |
| OpenTelemetry tracing | Community | ✅ | ✅ |
| Metrics | Community | ✅ | ✅ |
| Structured logging | Community | ✅ | ✅ |
| Correlation ID propagation | ❌ | ✅ | ✅ |
| Health checks | ❌ | ✅ | ✅ |
| **Workflows** | | | |
| Sagas | ❌ | ✅ | ✅ |
| Saga persistence (EF Core) | ❌ | ✅ | ✅ |
| State machines | ❌ | ✅ | ✅ |
| Scheduling / Recurring jobs | ❌ | ✅ | ✅ |
| **Persistence** | | | |
| Transactional outbox | ❌ | ✅ | ✅ |
| Inbox (deduplication) | ❌ | ✅ | ✅ |
| Audit trail | ❌ | ❌ | ✅ |
| EF Core integration | ❌ | ✅ | ✅ |
| **Enterprise** | | | |
| Authorization | ❌ | ❌ | ✅ |
| Multi-tenancy | ❌ | ❌ | ✅ |
| Message deduplication | ❌ | ✅ | ✅ |
| **Distributed Messaging** | | | |
| RabbitMQ, Azure Service Bus, etc. | ❌ | ✅ | ❌ |
| Multiple transport adapters | ❌ | ✅ | ❌ |
| **Developer Experience** | | | |
| CLI tooling | ❌ | ❌ | ✅ |
| Testing utilities | ✅ | ✅ | ✅ |
| **Licensing** | | | |
| Free for all teams | ❌¹ | ❌² | ✅ |
¹ MediatR's Community license is free only for companies/individuals with less than $5M USD in annual gross revenue that have never received more than $10M in outside capital. Source: [mediatr.io](https://mediatr.io).
² MassTransit v9 transitioned to a commercial licensing model. v8 remains open-source but will only receive security patches until end of 2026. Pricing starts at $400/month for small/medium businesses. Source: [MassTransit v9 announcement](https://masstransit.io/introduction/v9-announcement).
***
When to Choose What [#when-to-choose-what]
Choose TurboMediator when [#choose-turbomediator-when]
* You need **in-process messaging** with the fastest possible dispatch (source-generated, no reflection)
* You want **AOT / trimming** support
* You want a **complete pipeline ecosystem** — resilience, validation, caching, sagas, scheduling, observability — all built to work together
* You care about **compile-time safety** (missing handlers are caught at build time)
* You need **enterprise features** like authorization, multi-tenancy, or an audit trail without stitching together separate libraries
Choose MediatR when [#choose-mediatr-when]
* You have a **simple project** that only needs request/response dispatch and pipeline behaviors
* Your team is already familiar with it and the use case doesn't justify a migration
* Budget and licensing are not a concern
Choose MassTransit when [#choose-masstransit-when]
* You need **distributed messaging** across services (RabbitMQ, Azure Service Bus, Amazon SQS, etc.)
* Your architecture is **event-driven across process boundaries** rather than within a single process
* You need **distributed transaction choreography** with Routing Slips
MassTransit and TurboMediator are **not mutually exclusive**. A common architecture uses MassTransit for cross-service messaging and TurboMediator for in-process command/query dispatch within each service.
***
Performance Difference [#performance-difference]
The main performance differentiator is how message dispatch works under the hood.
| Approach | Dispatch mechanism | Runtime cost |
| ------------------------ | ---------------------------------------------- | -------------------------------- |
| MediatR | Reflection + `Dictionary` lookup | Allocation + reflection per call |
| MassTransit (in-process) | Expression-compiled delegates | One-time compilation cost |
| TurboMediator | Source-generated `switch` expression | Zero — resolved at compile time |
TurboMediator's source generator emits code like this at build time:
```csharp
// Generated — no reflection, no dictionary lookup
public ValueTask Send(IRequest request, CancellationToken ct) =>
request switch
{
CreateOrderCommand r => (ValueTask)(object)_createOrderHandler.Handle(r, ct),
GetOrderQuery r => (ValueTask)(object)_getOrderHandler.Handle(r, ct),
// ... all registered handlers
_ => ThrowUnknownRequest(request)
};
```
This means **no heap allocations** for the dispatch itself and full **inlining** by the JIT.
# TurboMediator
import { Cards, Card } from 'fumadocs-ui/components/card';
import { Callout } from 'fumadocs-ui/components/callout';
TurboMediator is currently in **preview**. APIs may change between releases without prior notice. It is usable in production, but exercise caution: thoroughly test in your environment, pin to an exact version in your dependencies, and review the changelog before upgrading.
What is TurboMediator? [#what-is-turbomediator]
TurboMediator is a **high-performance implementation** of the Mediator pattern for .NET that uses **Roslyn Source Generators** to create compile-time optimized dispatch code. Unlike traditional mediator libraries that rely on reflection and runtime type resolution, TurboMediator generates all the routing code at build time.
Key Benefits [#key-benefits]
* **Zero Reflection** — All handler dispatch is generated at compile time via `switch` expressions
* **Native AOT Compatible** — No runtime code generation, fully compatible with Native AOT publishing
* **Compile-Time Validation** — Missing handlers, duplicate handlers, and signature mismatches are caught as build errors
* **ValueTask-Based** — Uses `ValueTask` throughout for minimal allocation overhead
* **CQRS Ready** — Built-in support for Commands, Queries, Requests, and Notifications
* **Streaming** — First-class `IAsyncEnumerable` support for streaming scenarios
* **Modular** — 20+ optional packages for cross-cutting concerns
Package Overview [#package-overview]
| Package | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------ |
| `TurboMediator.Abstractions` | Core interfaces, message types, handlers, pipeline behaviors |
| `TurboMediator.SourceGenerator` | Compile-time code generation (required) |
| `TurboMediator.FluentValidation` | FluentValidation integration |
| `TurboMediator.Resilience` | Retry, Circuit Breaker, Timeout, Fallback, Hedging |
| `TurboMediator.Result` | Result pattern types for functional error handling |
| `TurboMediator.Observability` | Telemetry, Metrics, Correlation, Structured Logging, Health Checks |
| `TurboMediator.Caching` | Response caching with attribute-based configuration |
| `TurboMediator.Caching.Redis` | Redis distributed cache provider for TurboMediator.Caching |
| `TurboMediator.Validation` | Built-in lightweight validation (no FluentValidation dependency) |
| `TurboMediator.Enterprise` | Authorization, Multi-Tenancy, Deduplication |
| `TurboMediator.Scheduling` | Cron jobs and recurring job scheduling |
| `TurboMediator.Scheduling.EntityFramework` | EF Core job store for Scheduling |
| `TurboMediator.RateLimiting` | Rate Limiting & Bulkhead Isolation |
| `TurboMediator.Persistence` | Transactions, Outbox, Audit |
| `TurboMediator.Persistence.EntityFramework` | Entity Framework Core implementations |
| `TurboMediator.Saga` | Saga orchestration pattern |
| `TurboMediator.Saga.EntityFramework` | EF Core saga store |
| `TurboMediator.StateMachine` | State machine pattern with guards, transitions, and mediator integration |
| `TurboMediator.StateMachine.EntityFramework` | EF Core transition store for state machine auditing |
| `TurboMediator.Batching` | Batch processing for queries |
| `TurboMediator.FeatureFlags` | Feature flag behavior |
| `TurboMediator.FeatureFlags.FeatureManagement` | Microsoft.FeatureManagement integration |
| `TurboMediator.Testing` | Test utilities, fakes, and helpers |
| `TurboMediator.Cli` | CLI tool for analysis and docs generation |
Next Steps [#next-steps]
# Vibecoded with Opus
import { Callout } from 'fumadocs-ui/components/callout';
Yes, This Library Is Vibecoded [#yes-this-library-is-vibecoded]
TurboMediator was built entirely through **AI-assisted development** — specifically using [Claude Opus](https://www.anthropic.com/claude), Anthropic's most capable model. Every module, every abstraction, every source generator, every pipeline behavior was authored through a collaborative loop between human intent and AI generation.
There was no traditional keyboard-driven coding session. The architecture was described, the constraints were stated, the tradeoffs were discussed — and the code emerged from that conversation.
This is **vibecoding**: directing software into existence through high-bandwidth dialogue with a model capable of holding an entire codebase's worth of context.
***
Why That Should Concern You (and Why It Doesn't) [#why-that-should-concern-you-and-why-it-doesnt]
The reasonable reaction to hearing a library is vibecoded is skepticism. AI models hallucinate. They produce plausible-looking code that subtly violates invariants. They miss edge cases that a battle-scarred engineer would catch in seconds. They optimize for code that *reads* correct, not code that *is* correct.
These are legitimate concerns. But they apply to **the generation process**, not the **artifact**.
The test suite is the equalizer.
***
The Test Suite [#the-test-suite]
TurboMediator has a comprehensive suite of **unit and integration tests** covering every layer of the library:
Unit Tests [#unit-tests]
Every core behavior is tested in isolation:
| Area | Test File |
| ------------------------- | ------------------------------------------------------- |
| Commands | `CommandTests.cs` |
| Queries | `QueryTests.cs` |
| Requests | `RequestTests.cs` |
| Notifications | `NotificationTests.cs`, `NotificationPublisherTests.cs` |
| Pipeline behaviors | `PipelineBehaviorTests.cs` |
| Exception handling | `ExceptionHandlerTests.cs` |
| Streaming | `StreamingTests.cs` |
| Dependency injection | `DependencyInjectionTests.cs` |
| Validation | `ValidationTests.cs`, `FluentValidationTests.cs` |
| Caching | `CachingTests.cs` |
| Batching | `BatchingTests.cs` |
| Rate limiting | `RateLimiting/` |
| Resilience | `ResilienceTests.cs`, `Resilience/` |
| Feature flags | `FeatureFlagTests.cs` |
| Result types | `ResultTests.cs` |
| Sagas | `SagaTests.cs` |
| State machines | `StateMachineTests.cs` |
| Telemetry / Observability | `TelemetryTests.cs`, `Observability/` |
| Distributed locking | `DistributedLocking/` |
| Inbox / Outbox | `Inbox/`, `Outbox/` |
| Processors | `ProcessorTests.cs` |
| Enterprise features | `Enterprise/` |
| Scheduling | `Scheduling/` |
| CLI | `Cli/` |
| Testing utilities | `Testing/` |
Integration Tests [#integration-tests]
End-to-end scenarios test real interactions between components, including database-backed stores and external infrastructure:
| Test | What it covers |
| ----------------------------------------- | --------------------------------------------------------------- |
| `FullPipelineIntegrationTests.cs` | A complete request flowing through all pipeline behaviors |
| `CachingIntegrationTests.cs` | Distributed cache read/write/invalidation under a real pipeline |
| `TransactionIntegrationTests.cs` | Unit-of-work and transactional consistency with EF Core |
| `AuditStoreIntegrationTests.cs` | Audit trail persistence for commands |
| `SagaStoreIntegrationTests.cs` | Saga state persistence and resumption |
| `InboxStoreIntegrationTests.cs` | Inbox idempotency guarantees |
| `OutboxStoreIntegrationTests.cs` | Outbox reliable delivery semantics |
| `RedisDistributedLockIntegrationTests.cs` | Distributed locking via Redis |
AOT Compatibility [#aot-compatibility]
A dedicated `AotCompatibilityTests.cs` verifies that the source-generated output is fully compatible with **Native AOT** — no reflection, no runtime code emission, no trimming surprises.
***
Benchmark Results [#benchmark-results]
Numbers don't care who wrote the code. These results were produced by BenchmarkDotNet running against TurboMediator, MediatR, and the [Mediator](https://github.com/martinothamar/Mediator) source-generator library (the closest architectural equivalent).
> Intel Core i7-10750H, .NET 8.0.23, X64 RyuJIT AVX2
Send Command [#send-command]
| Method | Mean | Ratio | Allocated | Alloc Ratio |
| ------------------- | --------: | -----------: | --------: | ----------: |
| TurboMediator | 55.56 ns | baseline | 24 B | |
| Mediator\_SourceGen | 55.91 ns | 1.01x slower | 24 B | 1.00x more |
| MediatR | 219.16 ns | 3.94x slower | 296 B | 12.33x more |
Send Query [#send-query]
| Method | Mean | Ratio | Allocated | Alloc Ratio |
| ------------------- | --------: | -----------: | --------: | ----------: |
| TurboMediator | 77.29 ns | baseline | 48 B | |
| Mediator\_SourceGen | 68.36 ns | 1.13x faster | 48 B | 1.00x more |
| MediatR | 235.85 ns | 3.05x slower | 464 B | 9.67x more |
Send Command With Response [#send-command-with-response]
| Method | Mean | Ratio | Allocated | Alloc Ratio |
| ------------------- | --------: | -----------: | --------: | ----------: |
| TurboMediator | 72.76 ns | baseline | 48 B | |
| Mediator\_SourceGen | 66.50 ns | 1.09x faster | 48 B | 1.00x more |
| MediatR | 248.88 ns | 3.42x slower | 464 B | 9.67x more |
Pipeline Behavior (validation + logging + caching behaviors active) [#pipeline-behavior-validation--logging--caching-behaviors-active]
| Method | Mean | Ratio | Allocated | Alloc Ratio |
| ------------------- | --------: | -----------: | --------: | ----------: |
| TurboMediator | 74.81 ns | baseline | 48 B | |
| Mediator\_SourceGen | 78.64 ns | 1.05x slower | 48 B | 1.00x more |
| MediatR | 316.44 ns | 4.23x slower | 656 B | 13.67x more |
Publish Notification [#publish-notification]
| Method | Mean | Ratio | Allocated | Alloc Ratio |
| ------------------- | --------: | -----------: | --------: | ----------: |
| Mediator\_SourceGen | 57.20 ns | 1.48x faster | 24 B | 1.00x more |
| TurboMediator | 84.40 ns | baseline | 24 B | |
| MediatR | 216.04 ns | 2.56x slower | 344 B | 14.33x more |
TurboMediator is **3–4× faster than MediatR** and allocates **10–14× less memory** per operation. It runs at parity with the other source-generator-based implementation. This is what compile-time dispatch with zero reflection produces — and the AI wrote every line of it.
***
How the Development Process Worked [#how-the-development-process-worked]
Vibecoding is not "paste requirements, accept all." The loop looked like this:
1. **Describe the behavior** — state the interface contract, the invariants, the failure modes, and the performance constraints in precise terms
2. **Generate** — the model produces an implementation against those constraints
3. **Run the tests** — the test suite either passes or reveals where the model's mental model diverged from reality
4. **Review and challenge** — every non-trivial design decision was questioned: *why this abstraction? what does this break? is this the right tradeoff?*
5. **Iterate** — the loop repeats, refining until the tests pass and the design holds up under scrutiny
No code entered the codebase without passing the test suite. No design decision was accepted without a rationale that survived a challenge. The model generates fast; the human stays critical.
***
How It Compares [#how-it-compares]
If the vibecoded origin still bothers you, the [comparison page](/docs/comparison) benchmarks TurboMediator against MediatR feature-by-feature — licensing, performance, Native AOT support, pipeline capabilities, and ecosystem coverage.
The argument against vibecoded code is that it might be subtly wrong. The comparison shows what "subtly wrong" would have to overcome: a 4× performance advantage, zero allocations in the hot path, Native AOT compatibility, and 20+ modules — all with a passing test suite.
***
The Point [#the-point]
Code quality is not a function of who — or what — wrote it. It is a function of how thoroughly it is **specified**, **reviewed**, and **tested**.
Every behavior in this library carries a test. Every edge case that was identified during development was encoded as an assertion. The test suite is the specification made executable.
An AI that produces a correct, well-tested implementation is more useful than a human who produces an untested one. The output is what matters, and the output can be verified.
Open an issue. That's exactly what open source exists for — regardless of who wrote the code. The repository is public, the tests are the ground truth, and a reproducible bug report is always welcome.
This page exists because hiding the development process would be dishonest. Every module was reviewed critically before being accepted — the AI generated, the human decided. If you want to audit a specific design decision, the source is open.
# Why TurboMediator?
import { Callout } from 'fumadocs-ui/components/callout';
The Elephant in the Room [#the-elephant-in-the-room]
In early 2025, within days of each other, both MediatR and MassTransit announced their transition to commercial licensing models.
The two most widely-used .NET messaging libraries going paid almost simultaneously sent shockwaves through the community. A wave of blog posts and conference talks argued you should **stop using the Mediator pattern altogether**.
The most common reactions are:
* *"ASP.NET Core already has middleware and filters."*
* *"It's just an unnecessary abstraction over method calls."*
* *"You can call your services directly."*
These arguments are not wrong — they're just **incomplete**. They focus on a narrow slice of .NET development (small Web API projects) and ignore the reality of large, complex systems where the pattern truly shines.
Let's understand **why** this perception exists before we break down the specific arguments.
***
Why People Don't See the Value [#why-people-dont-see-the-value]
The dismissal of the Mediator pattern isn't random — it follows a predictable pattern rooted in how the community was exposed to it.
1. Every tutorial shows a TODO app [#1-every-tutorial-shows-a-todo-app]
The vast majority of MediatR tutorials demonstrate the pattern with a trivial CRUD API: create a `CreateTodoCommand`, wire up a handler, and... that's it. In that context, the pattern genuinely **is** unnecessary overhead. You added an interface, a handler class, and a pipeline — to call `_db.Add(todo)`.
The problem isn't the pattern. The problem is that **no one shows the pipeline**. The real power of the Mediator pattern — behaviors for validation, logging, caching, transactions, retry policies, telemetry — is almost never demonstrated in introductory content. People form their opinion based on the simplest possible example and never see the 90% of value that comes from the pipeline.
2. Paid licensing triggered a backlash against the pattern itself [#2-paid-licensing-triggered-a-backlash-against-the-pattern-itself]
When MediatR and MassTransit announced commercial licenses within days of each other in April 2025, the community reaction wasn't just "let's find an alternative." A significant portion of developers took it as an opportunity to argue the patterns themselves were unnecessary. The reasoning went: *"If I have to pay for it, maybe I never needed it."*
The simultaneity made the backlash louder than it would have been otherwise — two separate communities reacting at the same time, feeding the same narrative.
But this conflates the **licensing model of specific libraries** with the **usefulness of architectural patterns**. The Mediator pattern and event-driven messaging existed long before these libraries and will exist long after. Licensing decisions are business decisions — they say nothing about whether a pattern solves a real problem.
3. People generalize from their own context [#3-people-generalize-from-their-own-context]
A developer building a 5-endpoint API with no background workers, no event consumers, and no complex cross-cutting concerns will correctly conclude that the pattern adds no value **for them**. The mistake is assuming that experience applies universally.
In large codebases with multiple entry points, dozens of developers, and strict requirements around auditability, resilience, and consistency — the pattern pays for itself many times over. But you can't see that from the outside looking in.
4. The "just call the service directly" trap [#4-the-just-call-the-service-directly-trap]
Direct service injection feels simpler because the complexity is **hidden**, not absent. In a small project, `controller → service → repository` works fine. But as the application grows, each service method gradually accumulates validation, logging, authorization checks, caching logic, and error handling — scattered inconsistently across the codebase.
The Mediator pattern makes this complexity **explicit and centralized**. It feels like more ceremony upfront because it forces you to name your operations and define your pipeline. That's not a weakness — it's the entire point.
5. Confusion between the pattern and the library [#5-confusion-between-the-pattern-and-the-library]
Many critiques of "the Mediator pattern" are actually critiques of a specific library's API or implementation choices. Service location concerns, runtime reflection, dictionary lookups — these are **implementation details**, not properties of the pattern itself.
A source-generated mediator (like TurboMediator) has none of these issues. The dispatch is a compile-time `switch` expression. There's no service locator. There's no reflection. Dismissing the pattern because of how one library implemented it is like dismissing dependency injection because one container was slow.
***
Now let's address the specific technical arguments.
***
"ASP.NET middleware is enough" [#aspnet-middleware-is-enough]
ASP.NET middleware and endpoint filters are powerful — **for HTTP request pipelines**. But what about:
* **Background workers** (`IHostedService`, `BackgroundService`) processing messages from queues?
* **Scheduled jobs** (Hangfire, Quartz.NET, custom cron jobs)?
* **gRPC services**?
* **SignalR hubs**?
* **Console applications** and CLI tools?
* **Event-driven architectures** where messages flow between bounded contexts?
In a real-world enterprise system, your business logic doesn't live exclusively behind HTTP endpoints. You often have **multiple entry points** — an API, several workers, scheduled tasks, event consumers — all executing the **same business operations**.
ASP.NET middleware **only covers HTTP**. The Mediator pipeline covers **everything**.
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Web API │ │ Worker │ │ Cron Job │ │ gRPC/Hub │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │
└────────────────┴────────────────┴────────────────┘
│
┌───────────▼───────────┐
│ Mediator Pipeline │
│ ┌─────────────────┐ │
│ │ Validation │ │
│ │ Logging │ │
│ │ Authorization │ │
│ │ Caching │ │
│ │ Transactions │ │
│ │ Retry/CB │ │
│ │ Telemetry │ │
│ └─────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Handler │ │
│ └─────────────┘ │
└───────────────────────┘
```
When you have a `CreateOrderCommand`, it doesn't matter if it was triggered by an API endpoint, a message from RabbitMQ, or a scheduled job. The **same pipeline behaviors** (validation, authorization, logging, transactions, telemetry) are applied consistently.
Without a mediator, you'd need to **duplicate** those cross-cutting concerns across every entry point — or build your own pipeline abstraction, which is exactly what a mediator is.
***
"It's just an extra layer of indirection" [#its-just-an-extra-layer-of-indirection]
Yes. That's the point.
Every useful pattern in software adds a layer of indirection in exchange for something valuable. Dependency injection is indirection. Interfaces are indirection. The question is: **what do you get in return?**
With the Mediator pattern, you get:
1. Decoupled Architecture [#1-decoupled-architecture]
Your controllers, workers, and jobs don't need to know which service handles what. They send a message and receive a response. This makes your code **easier to refactor**, **easier to test**, and **easier to reason about**.
```csharp
// Controller doesn't know or care who handles this
public async Task CreateOrder([FromBody] CreateOrderRequest dto)
{
var result = await mediator.Send(new CreateOrderCommand(dto.CustomerId, dto.Items));
return Ok(result);
}
// Worker doesn't know or care who handles this
public async Task ProcessMessage(QueueMessage msg)
{
var command = JsonSerializer.Deserialize(msg.Body);
await mediator.Send(command);
}
```
Both entry points share the **same command**, the **same handler**, and the **same pipeline** — zero duplication.
2. Uniform Cross-Cutting Concerns [#2-uniform-cross-cutting-concerns]
Without a mediator, here's what adding logging to every operation looks like:
```csharp
// ❌ Without mediator — you repeat this in every service method
public async Task CreateOrder(CreateOrderCommand command)
{
_logger.LogInformation("Creating order for {CustomerId}", command.CustomerId);
var sw = Stopwatch.StartNew();
try
{
// validate...
// authorize...
// actual logic...
_logger.LogInformation("Order created in {Elapsed}ms", sw.ElapsedMilliseconds);
return order;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create order");
throw;
}
}
```
With a mediator pipeline, you write it **once**:
```csharp
// ✅ With mediator — write once, apply to every operation
public class LoggingBehavior : IPipelineBehavior
where TRequest : IMessage
{
public async ValueTask Handle(
TRequest request, MessageHandlerDelegate next, CancellationToken ct)
{
_logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
var sw = Stopwatch.StartNew();
var response = await next(request, ct);
_logger.LogInformation("{Request} handled in {Elapsed}ms",
typeof(TRequest).Name, sw.ElapsedMilliseconds);
return response;
}
}
```
This applies **automatically** to every command, query, and request in your system. No manual wiring. No forgetting to add it to that new endpoint.
3. Consistent CQRS Semantics [#3-consistent-cqrs-semantics]
The Mediator pattern naturally maps to CQRS. Commands mutate state. Queries read state. Notifications broadcast events. This vocabulary becomes a **shared language** across your team:
* Seeing `ICommand` → you know it writes data
* Seeing `IQuery` → you know it reads data
* Seeing `INotification` → you know it's fire-and-forget
This isn't just ceremony — it's **enforced architecture** that prevents common mistakes like commands that secretly read data or queries that mutate state.
4. Testability [#4-testability]
Handlers are simple classes with a single `Handle` method. They're trivial to unit test in isolation — no HTTP context, no middleware chain, no controller setup:
```csharp
[Fact]
public async Task CreateOrder_ShouldReturnOrder()
{
var handler = new CreateOrderHandler(mockRepo);
var result = await handler.Handle(
new CreateOrderCommand("customer-1", items),
CancellationToken.None);
Assert.NotNull(result);
}
```
Pipeline behaviors can also be tested independently, ensuring your cross-cutting concerns work correctly without spinning up the entire application.
***
When Should You Use It? [#when-should-you-use-it]
Strongly Recommended [#strongly-recommended]
| Scenario | Why |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------- |
| **Multiple entry points** (API + Workers + Jobs) | Shared pipeline and handlers eliminate duplication |
| **Complex cross-cutting concerns** (auth, validation, caching, retry, transactions) | Pipeline behaviors apply them consistently |
| **Large teams** working on the same codebase | Clear message contracts reduce coordination overhead |
| **CQRS / Event-driven architectures** | Natural fit for command/query/event separation |
| **Microservices** with shared business logic | Handlers are portable across service boundaries |
| **Applications requiring auditability** | Pipeline behaviors can intercept and log every operation |
Consider It [#consider-it]
| Scenario | Why |
| ------------------------------------------------- | ----------------------------------------------------------------------- |
| **Medium-sized Web APIs** with growing complexity | Establishes patterns early before the codebase becomes hard to refactor |
| **Applications with strict testing requirements** | Handler isolation makes unit testing straightforward |
| **Teams adopting Clean/Hexagonal Architecture** | Mediator fits naturally as the application layer boundary |
Probably Not Needed [#probably-not-needed]
| Scenario | Why |
| --------------------------------------- | --------------------------------------------------- |
| **Small CRUD APIs** with 5-10 endpoints | The overhead of the pattern exceeds its benefits |
| **Prototypes and MVPs** | Speed of development matters more than architecture |
| **Simple scripts or utilities** | Direct method calls are simpler and sufficient |
Even in smaller projects, TurboMediator's source-generated approach adds **near-zero runtime overhead** — so the cost of adopting it is primarily the learning curve, not performance.
***
What About the "Service Class" Approach? [#what-about-the-service-class-approach]
A common alternative is injecting service classes directly:
```csharp
public class OrderController(IOrderService orderService)
{
public async Task Create(CreateOrderDto dto)
=> Ok(await orderService.CreateOrder(dto));
}
```
This works fine at small scale. But as your application grows:
* **Service classes accumulate methods** and become god objects.
* **Cross-cutting concerns are scattered** across services or duplicated.
* **Adding a new concern** (e.g., caching) means touching every service method.
* **Different entry points** (API vs. Worker) need to independently wire up the same logic.
The Mediator pattern avoids all of these by giving you **one handler per operation** and a **single pipeline** for concerns.
| | Service Classes | Mediator Pattern |
| -------------------------- | -------------------------------------- | ------------------------------------ |
| Adding validation | Modify each service method | Add one pipeline behavior |
| Adding logging | Modify each service method | Add one pipeline behavior |
| Adding caching | Modify each service method | Add one pipeline behavior |
| New entry point (Worker) | Wire up services + recreate middleware | Just send the same message |
| New team member onboarding | "Read these 15 service classes" | "Commands go here, queries go there" |
***
The Real Cost of Not Using It [#the-real-cost-of-not-using-it]
In large applications, the cost of **not** using a mediator becomes visible over time:
1. **Inconsistency** — Some operations have logging, some don't. Some have validation, some don't. New features ship without proper cross-cutting concerns because developers forget or don't know they exist.
2. **Duplication** — The same authorization check is written in three places. The same caching logic is copy-pasted. When a bug is found, only one copy gets fixed.
3. **Fragile refactoring** — Changing how a business operation works requires updating the controller, the worker, the job, and every test that sets up the full dependency chain.
4. **Difficult onboarding** — New developers have no clear pattern to follow. Every service does things slightly differently. Code reviews become debates about where logic should live.
With a mediator, these problems are **structurally prevented**. The pipeline enforces consistency, handlers enforce single responsibility, and message contracts enforce clear boundaries.
***
The Boilerplate Problem (Why I Built This) [#the-boilerplate-problem-why-i-built-this]
Every time I worked on a real project using a mediator library, I ended up rewriting the same things from scratch:
* Validation wired to FluentValidation
* Retry and circuit breaker via Polly
* Logging and tracing with OpenTelemetry and correlation IDs
* EF Core unit-of-work transaction behavior
* Per-user/per-tenant rate limiting
* Test helpers to mock the mediator in unit tests
None of it is hard. But it takes time, it drifts between projects, and you end up owning it forever. Every project had a slightly different version of the same `ValidationBehavior`, the same `LoggingBehavior`, the same test abstractions.
That's what TurboMediator is trying to fix. Not just the mediator dispatch, but the entire surrounding ecosystem — built once, tested together, and ready to use.
***
Why TurboMediator Specifically? [#why-turbomediator-specifically]
If you're convinced the pattern is valuable, why choose TurboMediator over alternatives?
* **Free. Always.** — MIT-licensed with no revenue thresholds, team size limits, or commercial restrictions. No license key to deploy. No pricing page to check before upgrading.
* **Source-generated dispatch** — No reflection, no `Dictionary` lookups. A `switch` expression routes messages at compile time.
* **Compile-time safety** — Forgot to create a handler? Missing a pipeline registration? The compiler tells you immediately — not a runtime exception in production.
* **Native AOT ready** — No runtime code generation means full compatibility with `PublishAot`.
* **20+ optional packages** — Validation, resilience, observability, persistence, sagas, rate limiting, feature flags — all built for the TurboMediator pipeline.
* **Zero lock-in** — Your handlers are plain classes. Your messages are plain records. Migrating away (if you ever want to) means removing the `ICommand` interface and calling the handler directly.
***
Summary [#summary]
The Mediator pattern is not about adding indirection for the sake of it. It's about giving your application a **central nervous system** — a single pipeline through which all operations flow, where cross-cutting concerns are applied **once** and **consistently**, regardless of whether the trigger is an HTTP request, a queue message, or a scheduled job.
If your application has more than one entry point, complex cross-cutting concerns, or a growing team — the Mediator pattern isn't just nice to have. It's a **force multiplier**.
# Handlers
import { Callout } from 'fumadocs-ui/components/callout';
Handlers contain the business logic that processes messages. Each message type has a corresponding handler interface. TurboMediator discovers handlers automatically at compile time via the source generator.
Handler Interfaces [#handler-interfaces]
| Handler Interface | Message Type | Return |
| -------------------------------------- | --------------------- | ---------------------- |
| `IRequestHandler` | `IRequest` | `ValueTask` |
| `IRequestHandler` | `IRequest` | `ValueTask` |
| `ICommandHandler` | `ICommand` | `ValueTask` |
| `ICommandHandler` | `ICommand` | `ValueTask` |
| `IQueryHandler` | `IQuery` | `ValueTask` |
| `INotificationHandler` | `INotification` | `ValueTask` |
Request Handlers [#request-handlers]
```csharp
public record PingRequest : IRequest;
public class PingHandler : IRequestHandler
{
public ValueTask Handle(PingRequest request, CancellationToken ct)
{
return new ValueTask("Pong!");
}
}
```
For requests without a response:
```csharp
public record LogEventRequest(string Message) : IRequest;
public class LogEventHandler : IRequestHandler
{
private readonly ILogger _logger;
public LogEventHandler(ILogger logger) => _logger = logger;
public ValueTask Handle(LogEventRequest request, CancellationToken ct)
{
_logger.LogInformation("Event: {Message}", request.Message);
return Unit.ValueTask; // or simply: return default;
}
}
```
Void message types (`IRequest`, `ICommand`) use the `Unit` type internally. In handlers you can return `Unit.ValueTask` (explicit) or `default` (shorthand) — both are equivalent.
Command Handlers [#command-handlers]
```csharp
public record CreateOrderCommand(string Product, int Quantity) : ICommand;
public class CreateOrderHandler : ICommandHandler
{
private readonly IOrderRepository _repository;
public CreateOrderHandler(IOrderRepository repository)
{
_repository = repository;
}
public async ValueTask Handle(CreateOrderCommand command, CancellationToken ct)
{
var order = new Order
{
Id = Guid.NewGuid(),
Product = command.Product,
Quantity = command.Quantity,
CreatedAt = DateTime.UtcNow
};
await _repository.AddAsync(order, ct);
return order;
}
}
```
Query Handlers [#query-handlers]
```csharp
public record GetOrderQuery(Guid Id) : IQuery;
public class GetOrderHandler : IQueryHandler
{
private readonly IOrderRepository _repository;
public GetOrderHandler(IOrderRepository repository)
{
_repository = repository;
}
public async ValueTask Handle(GetOrderQuery query, CancellationToken ct)
{
return await _repository.FindByIdAsync(query.Id, ct);
}
}
```
Notification Handlers [#notification-handlers]
Notifications support **multiple handlers**. All registered handlers are invoked when a notification is published:
```csharp
public record OrderPlacedNotification(Guid OrderId, string CustomerEmail) : INotification;
// Handler 1: Send email
public class OrderEmailHandler : INotificationHandler
{
private readonly IEmailService _emailService;
public OrderEmailHandler(IEmailService emailService) => _emailService = emailService;
public async ValueTask Handle(OrderPlacedNotification notification, CancellationToken ct)
{
await _emailService.SendOrderConfirmation(notification.CustomerEmail, notification.OrderId);
}
}
// Handler 2: Update analytics
public class OrderAnalyticsHandler : INotificationHandler
{
private readonly IAnalyticsService _analytics;
public OrderAnalyticsHandler(IAnalyticsService analytics) => _analytics = analytics;
public async ValueTask Handle(OrderPlacedNotification notification, CancellationToken ct)
{
await _analytics.TrackOrderPlaced(notification.OrderId);
}
}
// Handler 3: Update inventory
public class InventoryUpdateHandler : INotificationHandler
{
public ValueTask Handle(OrderPlacedNotification notification, CancellationToken ct)
{
Console.WriteLine($"Updating inventory for order {notification.OrderId}");
return default;
}
}
```
Dependency Injection [#dependency-injection]
Handlers support constructor injection. All dependencies are resolved from the DI container:
```csharp
public class CreateUserHandler : ICommandHandler
{
private readonly IUserRepository _repository;
private readonly ILogger _logger;
private readonly IMediator _mediator;
public CreateUserHandler(
IUserRepository repository,
ILogger logger,
IMediator mediator)
{
_repository = repository;
_logger = logger;
_mediator = mediator;
}
public async ValueTask Handle(CreateUserCommand command, CancellationToken ct)
{
_logger.LogInformation("Creating user {Name}", command.Name);
var user = new User(Guid.NewGuid(), command.Name, command.Email);
await _repository.AddAsync(user, ct);
// Publish notification from within a handler
await _mediator.Publish(new UserCreatedNotification(user.Id, user.Email), ct);
return user;
}
}
```
Each message (except notifications) must have **exactly one** handler. The source generator will emit:
* `TURBO001` warning if no handler is found
* `TURBO002` error if multiple handlers exist for the same message
* `TURBO008` error if the handler is not public
Handler Lifetime [#handler-lifetime]
By default, handlers are registered as **scoped** services — a new instance is created per request/scope. This is the safe default because handlers often inject scoped dependencies like `DbContext`, `ITenantContext`, or `HttpContext`.
```csharp
// Default — Scoped (recommended for most apps)
builder.Services.AddTurboMediator();
```
You can change the lifetime if your handlers are stateless and don't inject scoped services:
```csharp
// Singleton — reuses handler instances across all requests
// ⚠️ Only safe when handlers DON'T inject scoped services (DbContext, ITenantContext, etc.)
builder.Services.AddTurboMediator(ServiceLifetime.Singleton);
```
If handlers inject **scoped services** (e.g. EF Core `DbContext`, `ITenantContext`, `IHttpContextAccessor`, or any service registered with `AddScoped`), the lifetime **must** be `Scoped` (the default). Using `Singleton` will cause a runtime `InvalidOperationException`: *"Cannot consume scoped service from singleton"*.
The `IMediator` instance itself is stateless — it resolves handlers on-demand from the DI container. The `lifetime` parameter controls the registration of **both** the mediator and all handlers.
# Mediator Context
import { Callout } from 'fumadocs-ui/components/callout';
`IMediatorContext` provides a way to flow **contextual data** through the entire pipeline — from pre-processors, through behaviors, to the handler and post-processors.
IMediatorContext Interface [#imediatorcontext-interface]
```csharp
public interface IMediatorContext
{
string CorrelationId { get; set; }
string? CausationId { get; set; }
string? UserId { get; set; }
string? TenantId { get; set; }
string? TraceId { get; set; }
string? SpanId { get; set; }
IDictionary Items { get; }
// Keyed access
void Set(string key, T value);
T? Get(string key);
bool TryGet(string key, out T? value);
// Type-based access (uses type name as key)
void Set(T value);
T? Get();
bool TryGet(out T? value);
}
```
Built-in Properties [#built-in-properties]
| Property | Description |
| --------------- | ------------------------------------------------- |
| `CorrelationId` | Unique ID that groups related operations together |
| `CausationId` | ID of the message that caused this message |
| `UserId` | ID of the current user |
| `TenantId` | ID of the current tenant (multi-tenancy) |
| `TraceId` | Distributed tracing trace ID |
| `SpanId` | Distributed tracing span ID |
Using the Context [#using-the-context]
Setting up [#setting-up]
Register the mediator context with `WithMediatorContext()`:
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithMediatorContext();
});
```
Accessing in handlers [#accessing-in-handlers]
Inject `IMediatorContextAccessor` to access the current context:
```csharp
public class CreateOrderHandler : ICommandHandler
{
private readonly IMediatorContextAccessor _contextAccessor;
public CreateOrderHandler(IMediatorContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public async ValueTask Handle(CreateOrderCommand command, CancellationToken ct)
{
var context = _contextAccessor.Context;
var userId = context?.UserId;
var correlationId = context?.CorrelationId;
// Use context data...
return new Order { CreatedBy = userId };
}
}
```
Storing custom data [#storing-custom-data]
Use `Set(value)` and `Get()` for type-based access (uses the type name as the key), or `Set(key, value)` and `Get(key)` for explicit keys:
```csharp
// Type-based access — convenient when you have one value per type
context.Set(new RequestMetadata { IpAddress = "127.0.0.1" });
var metadata = context.Get();
// Keyed access — when you need multiple values of the same type
context.Set("SourceIp", ipAddress);
context.Set("TargetIp", targetAddress);
var source = context.Get("SourceIp");
```
Full behavior example:
```csharp
// In a pre-processor or behavior
public class EnrichContextBehavior
: IPipelineBehavior
where TMessage : IMessage
{
private readonly IMediatorContextAccessor _contextAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
public EnrichContextBehavior(
IMediatorContextAccessor contextAccessor,
IHttpContextAccessor httpContextAccessor)
{
_contextAccessor = contextAccessor;
_httpContextAccessor = httpContextAccessor;
}
public async ValueTask Handle(
TMessage message,
MessageHandlerDelegate next,
CancellationToken ct)
{
var context = _contextAccessor.Context!;
// Set typed data
context.Set("RequestMetadata", new RequestMetadata
{
IpAddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(),
UserAgent = _httpContextAccessor.HttpContext?.Request.Headers.UserAgent.ToString()
});
// Set keyed data
context.Items["StartTime"] = DateTime.UtcNow;
return await next(message, ct);
}
}
```
Child contexts [#child-contexts]
`MediatorContext` (the default implementation) supports `CreateChild()` to spawn sub-operations that inherit parent context:
```csharp
var childContext = context.CreateChild();
// Child inherits parent's CorrelationId but can have its own CausationId
```
IMediatorContextAccessor [#imediatorcontextaccessor]
Uses `AsyncLocal` under the hood, ensuring context isolation across async flows:
```csharp
public interface IMediatorContextAccessor
{
IMediatorContext? Context { get; set; }
}
```
The mediator context is especially useful when combined with the Correlation ID, Structured Logging, and Telemetry behaviors from the Observability package — they all read from and write to this context.
# Messages
import { Callout } from 'fumadocs-ui/components/callout';
TurboMediator follows the **CQRS** (Command Query Responsibility Segregation) pattern with clear message type separation. All messages flow through the mediator pipeline and are routed to their respective handlers.
Message Type Hierarchy [#message-type-hierarchy]
```
IMessage
├── IBaseRequest
│ ├── IRequest // Generic request
│ └── IRequest // Void request (returns Unit)
├── IBaseCommand
│ ├── ICommand // Command with result
│ └── ICommand // Void command (returns Unit)
└── IBaseQuery
└── IQuery // Query (always returns data)
INotification // Fire-and-forget, multi-handler
```
Requests [#requests]
Use `IRequest` for generic operations that don't fit neatly into Command or Query categories:
```csharp
// Request with a response
public record PingRequest : IRequest;
// Request without response (returns Unit)
public record LogEventRequest(string Message) : IRequest;
```
Commands [#commands]
Use `ICommand` for **write operations** that modify state:
```csharp
// Command with a response
public record CreateUserCommand(string Name, string Email) : ICommand;
// Command without response
public record DeleteUserCommand(Guid UserId) : ICommand;
// Command with result indicating success
public record UpdateOrderCommand(Guid OrderId, string Status) : ICommand;
```
Queries [#queries]
Use `IQuery` for **read operations** that return data without side effects:
```csharp
public record GetUserQuery(Guid Id) : IQuery;
public record GetAllUsersQuery : IQuery>;
public record SearchProductsQuery(string Term, int Page) : IQuery>;
```
Notifications [#notifications]
Use `INotification` for **events** that can have zero or more handlers:
```csharp
public record UserCreatedNotification(Guid UserId, string Email) : INotification;
public record OrderShippedNotification(Guid OrderId, DateTime ShippedAt) : INotification;
```
Unlike requests, commands, and queries, notifications can have **multiple handlers**. They are dispatched to all registered handlers based on the configured notification publisher strategy.
The Unit Type [#the-unit-type]
For messages that don't return a meaningful value, TurboMediator uses the `Unit` struct:
```csharp
// IRequest (no generic parameter) is equivalent to IRequest
public record DeleteUserCommand(Guid Id) : ICommand;
// Handler returns Unit
public class DeleteUserHandler : ICommandHandler
{
public ValueTask Handle(DeleteUserCommand command, CancellationToken ct)
{
// Perform deletion...
return default;
}
}
```
`Unit` provides static helpers:
* `Unit.Value` — The singleton `Unit` instance
* `Unit.ValueTask` — A pre-completed `ValueTask`
* `Unit.Task` — A pre-completed `Task`
Sending Messages [#sending-messages]
All messages are sent through the `IMediator` interface (or its sub-interfaces `ISender` and `IPublisher`):
```csharp
public class MyService
{
private readonly IMediator _mediator;
public MyService(IMediator mediator) => _mediator = mediator;
public async Task DoWork()
{
// Send a query
var user = await _mediator.Send(new GetUserQuery(someId));
// Send a command
var newUser = await _mediator.Send(new CreateUserCommand("Alice", "alice@example.com"));
// Send a request
var pong = await _mediator.Send(new PingRequest());
// Publish a notification
await _mediator.Publish(new UserCreatedNotification(newUser.Id, newUser.Email));
}
}
```
Best Practices [#best-practices]
1. **Use records** — `record` types provide immutability, value equality, and concise syntax
2. **Follow CQRS** — Use `ICommand` for writes, `IQuery` for reads
3. **Keep messages simple** — Messages should be plain data containers, no behavior
4. **Use meaningful names** — Suffix with `Command`, `Query`, `Request`, or `Notification`
5. **Prefer specific types** — Use `ICommand`/`IQuery` over `IRequest` when the intent is clear
# Notifications
import { Callout } from 'fumadocs-ui/components/callout';
Notifications are messages that can be handled by **zero or more** handlers. They are ideal for decoupling side effects from the main business logic.
Defining Notifications [#defining-notifications]
```csharp
public record UserRegisteredNotification(
Guid UserId,
string Email,
DateTime RegisteredAt
) : INotification;
```
Publishing Notifications [#publishing-notifications]
Use `IPublisher` (or `IMediator`) to publish:
```csharp
await mediator.Publish(new UserRegisteredNotification(
user.Id,
user.Email,
DateTime.UtcNow
));
```
Notification Publishers [#notification-publishers]
TurboMediator provides five built-in strategies for dispatching notifications to handlers:
ForeachAwaitPublisher (Default) [#foreachawaitpublisher-default]
Invokes handlers **sequentially**, one at a time. If a handler throws, subsequent handlers are not invoked.
```csharp
builder.Services.AddTurboMediator(m => m.WithSequentialNotifications());
```
TaskWhenAllPublisher [#taskwhenallpublisher]
Invokes all handlers **in parallel** using `Task.WhenAll`. All handlers run concurrently.
```csharp
builder.Services.AddTurboMediator(m => m.WithParallelNotifications());
```
FireAndForgetPublisher [#fireandforgetpublisher]
Dispatches handlers via `Task.Run` — **non-blocking**. The publish call returns immediately.
```csharp
builder.Services.AddTurboMediator(m => m.WithFireAndForgetNotifications());
```
Fire-and-forget notifications are not awaited. Exceptions in handlers will be lost unless you add your own error handling within the handlers.
ContinueOnExceptionPublisher [#continueonexceptionpublisher]
Invokes handlers **sequentially** but continues even if a handler throws. All exceptions are collected into an `AggregateException` thrown at the end.
```csharp
builder.Services.AddTurboMediator(m =>
m.WithNotificationPublisher(ContinueOnExceptionPublisher.Instance));
```
StopOnFirstExceptionPublisher [#stoponfirstexceptionpublisher]
Invokes handlers **sequentially** and stops immediately on the first exception.
```csharp
builder.Services.AddTurboMediator(m =>
m.WithNotificationPublisher(StopOnFirstExceptionPublisher.Instance));
```
Custom Notification Publisher [#custom-notification-publisher]
Implement `INotificationPublisher` to create your own strategy:
```csharp
public class PriorityNotificationPublisher : INotificationPublisher
{
public async ValueTask Publish(
INotificationHandler[] handlers,
TNotification notification,
CancellationToken cancellationToken)
where TNotification : INotification
{
// Custom dispatch logic
foreach (var handler in handlers)
{
await handler.Handle(notification, cancellationToken);
}
}
}
```
Missing Handler Behavior [#missing-handler-behavior]
By default, publishing a notification with no registered handlers is a no-op. You can change this:
```csharp
builder.Services.AddTurboMediator(m =>
m.ThrowOnNoNotificationHandler());
```
Practical Example [#practical-example]
```csharp
// Define the notification
public record PaymentProcessedNotification(
Guid OrderId,
decimal Amount,
string Currency
) : INotification;
// Handler 1: Send receipt
public class SendReceiptHandler : INotificationHandler
{
private readonly IEmailService _email;
public SendReceiptHandler(IEmailService email) => _email = email;
public async ValueTask Handle(PaymentProcessedNotification n, CancellationToken ct)
{
await _email.SendReceiptAsync(n.OrderId, n.Amount, n.Currency);
}
}
// Handler 2: Update accounting
public class UpdateAccountingHandler : INotificationHandler
{
private readonly IAccountingService _accounting;
public UpdateAccountingHandler(IAccountingService accounting) => _accounting = accounting;
public async ValueTask Handle(PaymentProcessedNotification n, CancellationToken ct)
{
await _accounting.RecordPaymentAsync(n.OrderId, n.Amount, n.Currency);
}
}
// Handler 3: Log for audit
public class PaymentAuditHandler : INotificationHandler
{
private readonly ILogger _logger;
public PaymentAuditHandler(ILogger logger) => _logger = logger;
public ValueTask Handle(PaymentProcessedNotification n, CancellationToken ct)
{
_logger.LogInformation("Payment of {Amount} {Currency} processed for order {OrderId}",
n.Amount, n.Currency, n.OrderId);
return default;
}
}
```
# Streaming
import { Callout } from 'fumadocs-ui/components/callout';
TurboMediator provides first-class support for **streaming responses** using `IAsyncEnumerable`. This is ideal for scenarios where you want to return data incrementally — large datasets, real-time feeds, or server-sent events.
Stream Message Types [#stream-message-types]
| Interface | Handler Interface |
| --------------------------- | -------------------------------------------- |
| `IStreamRequest` | `IStreamRequestHandler` |
| `IStreamCommand` | `IStreamCommandHandler` |
| `IStreamQuery` | `IStreamQueryHandler` |
Defining Stream Messages [#defining-stream-messages]
```csharp
// Stream request
public record GenerateNumbersRequest(int Count) : IStreamRequest;
// Stream query
public record GetAllUsersQuery : IStreamQuery;
// Stream command
public record ProcessItemsCommand(IReadOnlyList- Items) : IStreamCommand;
```
Stream Handlers [#stream-handlers]
Stream handlers return `IAsyncEnumerable`:
```csharp
public class GenerateNumbersHandler
: IStreamRequestHandler
{
public async IAsyncEnumerable Handle(
GenerateNumbersRequest request,
[EnumeratorCancellation] CancellationToken ct)
{
for (int i = 1; i <= request.Count; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(100, ct); // Simulate work
yield return i;
}
}
}
public class GetAllUsersHandler
: IStreamQueryHandler
{
private readonly IUserRepository _repository;
public GetAllUsersHandler(IUserRepository repository)
{
_repository = repository;
}
public async IAsyncEnumerable Handle(
GetAllUsersQuery query,
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var user in _repository.GetAllAsync(ct))
{
yield return user;
}
}
}
```
Consuming Streams [#consuming-streams]
Use `CreateStream` on the mediator to get the `IAsyncEnumerable`:
```csharp
// Consume a stream
await foreach (var number in mediator.CreateStream(new GenerateNumbersRequest(10)))
{
Console.WriteLine($"Received: {number}");
}
// Use with LINQ (System.Linq.Async)
var users = await mediator
.CreateStream(new GetAllUsersQuery())
.Where(u => u.IsActive)
.Take(50)
.ToListAsync();
```
Stream Pipeline Behaviors [#stream-pipeline-behaviors]
Streaming has dedicated pipeline interfaces:
IStreamPipelineBehavior [#istreampipelinebehavior]
Wraps the entire stream execution:
```csharp
public class StreamLoggingBehavior
: IStreamPipelineBehavior
where TMessage : IStreamMessage
{
private readonly ILogger _logger;
public StreamLoggingBehavior(ILogger> logger)
{
_logger = logger;
}
public async IAsyncEnumerable Handle(
TMessage message,
StreamHandlerDelegate next,
[EnumeratorCancellation] CancellationToken ct)
{
_logger.LogInformation("Starting stream for {MessageType}", typeof(TMessage).Name);
var count = 0;
await foreach (var item in next())
{
count++;
yield return item;
}
_logger.LogInformation("Stream {MessageType} completed with {Count} items",
typeof(TMessage).Name, count);
}
}
```
IStreamPreProcessor [#istreampreprocessor]
Runs before the stream starts:
```csharp
public class StreamValidationPreProcessor
: IStreamPreProcessor
where TMessage : IStreamMessage
{
public ValueTask Process(TMessage message, CancellationToken ct)
{
// Validate message before streaming starts
return default;
}
}
```
IStreamPostProcessor [#istreampostprocessor]
Wraps the stream after the handler — can transform, log, or add metrics to each item:
```csharp
public class StreamMetricsPostProcessor
: IStreamPostProcessor
where TMessage : IStreamMessage
{
public async IAsyncEnumerable Process(
TMessage message,
IAsyncEnumerable stream,
[EnumeratorCancellation] CancellationToken ct)
{
var count = 0;
await foreach (var item in stream.WithCancellation(ct))
{
count++;
yield return item;
}
// Record metrics after stream completes
}
}
```
Registering Stream Behaviors [#registering-stream-behaviors]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithStreamPipelineBehavior>();
m.WithStreamPreProcessor>();
m.WithStreamPostProcessor>();
});
```
SSE Example (Minimal API) [#sse-example-minimal-api]
```csharp
app.MapGet("/stream/numbers", async (HttpContext context, IMediator mediator) =>
{
context.Response.ContentType = "text/event-stream";
await foreach (var number in mediator.CreateStream(new GenerateNumbersRequest(100)))
{
await context.Response.WriteAsync($"data: {number}\n\n");
await context.Response.Body.FlushAsync();
}
});
```
Stream pipeline behaviors use separate interfaces (`IStreamPipelineBehavior`, `IStreamPreProcessor`, `IStreamPostProcessor`) that are independent from the regular pipeline behaviors.
# Authorization
import { Callout } from 'fumadocs-ui/components/callout';
The authorization behavior provides attribute-based access control for mediator operations, checking roles and policies before the handler executes.
Installation [#installation]
```bash
dotnet add package TurboMediator.Enterprise
```
Using Attributes [#using-attributes]
[Authorize] [#authorize]
```csharp
// Require authentication
[Authorize]
public record GetProfileQuery : IQuery;
// Require specific role
[Authorize(Roles = "Admin")]
public record DeleteUserCommand(Guid UserId) : ICommand;
// Require a policy
[Authorize(Policy = "CanManageOrders")]
public record CancelOrderCommand(Guid OrderId) : ICommand;
// Multiple requirements
[Authorize(Roles = "Admin,Manager", Policy = "CanEditProducts")]
public record UpdateProductCommand(Guid ProductId, string Name, decimal Price) : ICommand;
// Authentication scheme
[Authorize(AuthenticationSchemes = "Bearer")]
public record GetApiDataQuery : IQuery;
```
[AllowAnonymous] [#allowanonymous]
Override `[Authorize]` on specific operations:
```csharp
[AllowAnonymous]
public record GetPublicProductsQuery : IQuery>;
```
IUserContext [#iusercontext]
Implement `IUserContext` to provide user information:
```csharp
public interface IUserContext
{
ClaimsPrincipal? User { get; }
bool IsAuthenticated { get; }
}
// ASP.NET Core implementation
public class HttpUserContext : IUserContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpUserContext(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User;
public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false;
}
```
IAuthorizationPolicyProvider [#iauthorizationpolicyprovider]
Implement custom policy evaluation:
```csharp
public class CustomPolicyProvider : IAuthorizationPolicyProvider
{
public ValueTask EvaluatePolicyAsync(
ClaimsPrincipal user,
string policy,
CancellationToken ct)
{
return policy switch
{
"CanManageOrders" => new ValueTask(
user.HasClaim("permission", "orders:manage")),
"CanEditProducts" => new ValueTask(
user.IsInRole("Admin") || user.HasClaim("permission", "products:edit")),
_ => new ValueTask(false)
};
}
}
```
Registration [#registration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithAuthorization();
m.WithUserContext();
m.WithAuthorizationPolicies(policies =>
{
policies.AddPolicy("CanManageOrders", u => u.HasClaim("permission", "orders:manage"));
policies.AddPolicy("CanEditProducts", u =>
u.IsInRole("Admin") || u.HasClaim("permission", "products:edit"));
});
});
```
UnauthorizedException [#unauthorizedexception]
Thrown when authorization fails:
```csharp
try
{
await mediator.Send(new DeleteUserCommand(userId));
}
catch (UnauthorizedException ex)
{
// ex.MessageType — the message type that was denied
// ex.Policy — the policy that failed
// ex.RequiredRoles — required roles that the user doesn't have
return Results.Forbid();
}
```
Practical Example [#practical-example]
```csharp
// Define messages with authorization
[Authorize(Roles = "Admin")]
public record PromoteUserCommand(Guid UserId, string NewRole) : ICommand;
[Authorize(Policy = "CanViewReports")]
public record GetSalesReportQuery(DateRange Range) : IQuery;
[AllowAnonymous]
public record GetPublicStatsQuery : IQuery;
// Register
builder.Services.AddHttpContextAccessor();
builder.Services.AddTurboMediator(m =>
{
m.WithAuthorization();
m.WithUserContext();
});
// Usage in API
app.MapDelete("/users/{id}", async (Guid id, IMediator mediator) =>
{
try
{
await mediator.Send(new PromoteUserCommand(id, "Manager"));
return Results.Ok();
}
catch (UnauthorizedException)
{
return Results.Forbid();
}
});
```
The authorization behavior checks `[AllowAnonymous]` first. If present, authorization is skipped entirely. Otherwise, it validates authentication, roles, and policies in order.
# Deduplication
import { Callout } from 'fumadocs-ui/components/callout';
The deduplication behavior prevents the same message from being processed more than once by using idempotency keys and a pluggable store.
IIdempotentMessage [#iidempotentmessage]
Mark messages that support deduplication:
```csharp
public interface IIdempotentMessage
{
string IdempotencyKey { get; }
}
// Example
public record ProcessPaymentCommand(
Guid PaymentId,
decimal Amount,
string Currency
) : ICommand, IIdempotentMessage
{
public string IdempotencyKey => $"payment:{PaymentId}";
}
```
IIdempotencyStore [#iidempotencystore]
```csharp
public interface IIdempotencyStore
{
ValueTask TryAcquireAsync(string key, TimeSpan ttl, CancellationToken ct);
ValueTask?> GetAsync(string key, CancellationToken ct);
ValueTask SetAsync(string key, T response, TimeSpan ttl, CancellationToken ct);
ValueTask ReleaseAsync(string key, CancellationToken ct);
}
```
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithDeduplication(options =>
{
options.TimeToLive = TimeSpan.FromHours(24);
options.ThrowOnDuplicate = false;
options.MaxWaitRetries = 5;
options.WaitRetryInterval = TimeSpan.FromMilliseconds(200);
options.ReleaseOnError = true;
});
m.WithInMemoryIdempotencyStore();
});
```
DeduplicationOptions [#deduplicationoptions]
| Option | Default | Description |
| ------------------- | -------- | ------------------------------------------------------ |
| `TimeToLive` | 24 hours | How long idempotency records are kept |
| `ThrowOnDuplicate` | `false` | Throw exception on duplicate vs return cached response |
| `MaxWaitRetries` | 5 | Retries when a concurrent duplicate is being processed |
| `WaitRetryInterval` | 200ms | Delay between retries |
| `ReleaseOnError` | `true` | Release lock if handler throws |
How It Works [#how-it-works]
1. Checks if `IdempotencyKey` exists in the store
2. If found → return cached response (or throw if `ThrowOnDuplicate`)
3. If not found → acquire lock and execute handler
4. Store the response with the key
5. If handler throws and `ReleaseOnError` is true → release the lock
For concurrent duplicate requests:
1. First request acquires the lock and processes
2. Second request waits (up to `MaxWaitRetries`) for the first to complete
3. Returns the cached response once available
Practical Example [#practical-example]
```csharp
public record ChargeCustomerCommand(
string PaymentIntentId,
decimal Amount,
string CustomerId
) : ICommand, IIdempotentMessage
{
public string IdempotencyKey => $"charge:{PaymentIntentId}";
}
public class ChargeCustomerHandler
: ICommandHandler
{
private readonly IPaymentGateway _gateway;
public ChargeCustomerHandler(IPaymentGateway gateway) => _gateway = gateway;
public async ValueTask Handle(
ChargeCustomerCommand command, CancellationToken ct)
{
// Safe to execute — deduplication ensures this runs only once
// per PaymentIntentId, even with network retries
return await _gateway.ChargeAsync(
command.CustomerId, command.Amount, ct);
}
}
```
Deduplication is essential for webhook handlers, payment processing, and any operation where retries from external systems could cause duplicates.
# Multi-Tenancy
import { Callout } from 'fumadocs-ui/components/callout';
The multi-tenancy behavior ensures tenant context is present and validated for operations that require it.
Core Interfaces [#core-interfaces]
ITenantContext [#itenantcontext]
Provides current tenant information:
```csharp
public interface ITenantContext
{
string? TenantId { get; }
bool HasTenant { get; }
string? TenantName { get; }
}
```
ITenantAware [#itenantaware]
Mark messages that belong to a specific tenant:
```csharp
public interface ITenantAware
{
string? TenantId { get; }
}
```
Using the Attribute [#using-the-attribute]
```csharp
[RequiresTenant]
public record GetTenantDataQuery(string DataKey) : IQuery;
```
Tenant-Aware Messages [#tenant-aware-messages]
```csharp
public record CreateTenantDocumentCommand(
string TenantId,
string Title,
string Content
) : ICommand, ITenantAware;
```
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithMultiTenancy();
m.WithTenantContext();
});
```
Implementing ITenantContext [#implementing-itenantcontext]
```csharp
public class HttpTenantContext : ITenantContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpTenantContext(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? TenantId =>
_httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault()
?? _httpContextAccessor.HttpContext?.User?.FindFirst("tenant_id")?.Value;
public bool HasTenant => !string.IsNullOrEmpty(TenantId);
public string? TenantName =>
_httpContextAccessor.HttpContext?.User?.FindFirst("tenant_name")?.Value;
}
```
Validation Rules [#validation-rules]
The `TenantBehavior` validates:
1. If `[RequiresTenant]` is present, `ITenantContext.HasTenant` must be `true`
2. If the message implements `ITenantAware`, the message's `TenantId` must match `ITenantContext.TenantId`
3. Throws an exception if validation fails
Practical Example [#practical-example]
```csharp
// Tenant-isolated queries
[RequiresTenant]
public record GetTenantUsersQuery : IQuery>;
public class GetTenantUsersHandler : IQueryHandler>
{
private readonly ITenantContext _tenantContext;
private readonly IUserRepository _repository;
public GetTenantUsersHandler(ITenantContext tenantContext, IUserRepository repository)
{
_tenantContext = tenantContext;
_repository = repository;
}
public async ValueTask> Handle(
GetTenantUsersQuery query, CancellationToken ct)
{
// TenantId is guaranteed to be present by the behavior
return await _repository.GetByTenantAsync(_tenantContext.TenantId!, ct);
}
}
```
The tenant behavior should be registered **before** the handler in the pipeline to ensure tenant validation happens early. Register it after authentication/authorization.
# Installation
Requirements [#requirements]
* .NET 8.0+ (or .NET Standard 2.0 for the abstractions)
* C# 11+ (for source generator support)
Core Packages [#core-packages]
Every TurboMediator project requires these two packages:
```bash title="Terminal"
dotnet add package TurboMediator.Abstractions
dotnet add package TurboMediator.SourceGenerator
```
The **Abstractions** package contains all core interfaces (`IRequest`, `ICommand`, `IQuery`, `INotification`, handlers, pipeline behaviors, etc.).
The **SourceGenerator** package contains the Roslyn incremental source generator that produces the `Mediator` implementation class at compile time.
Optional Packages [#optional-packages]
Install only the packages you need. Each package is independent and adds specific pipeline behaviors:
```bash title="Validation"
dotnet add package TurboMediator.FluentValidation
```
```bash title="Resilience (Retry, Circuit Breaker, Timeout, Fallback, Hedging)"
dotnet add package TurboMediator.Resilience
```
```bash title="Result Pattern (Functional Error Handling)"
dotnet add package TurboMediator.Result
```
```bash title="Observability (Telemetry, Metrics, Correlation, Logging, Health Checks)"
dotnet add package TurboMediator.Observability
```
```bash title="Caching (In-Memory, Custom Providers)"
dotnet add package TurboMediator.Caching
```
```bash title="Caching - Redis Provider"
dotnet add package TurboMediator.Caching.Redis
```
```bash title="Validation (Built-in Lightweight Validator)"
dotnet add package TurboMediator.Validation
```
```bash title="Enterprise (Authorization, Multi-Tenancy, Deduplication)"
dotnet add package TurboMediator.Enterprise
```
```bash title="Scheduling (Cron Jobs, Recurring Jobs)"
dotnet add package TurboMediator.Scheduling
dotnet add package TurboMediator.Scheduling.EntityFramework
```
```bash title="Rate Limiting & Bulkhead"
dotnet add package TurboMediator.RateLimiting
```
```bash title="Distributed Locking"
dotnet add package TurboMediator.DistributedLocking
dotnet add package TurboMediator.DistributedLocking.Redis
```
```bash title="Persistence (Transactions, Outbox, Audit)"
dotnet add package TurboMediator.Persistence
dotnet add package TurboMediator.Persistence.EntityFramework
```
```bash title="Saga"
dotnet add package TurboMediator.Saga
dotnet add package TurboMediator.Saga.EntityFramework
```
```bash title="State Machine"
dotnet add package TurboMediator.StateMachine
dotnet add package TurboMediator.StateMachine.EntityFramework
```
```bash title="Feature Flags"
dotnet add package TurboMediator.FeatureFlags
dotnet add package TurboMediator.FeatureFlags.FeatureManagement
```
```bash title="Batching"
dotnet add package TurboMediator.Batching
```
```bash title="Testing"
dotnet add package TurboMediator.Testing
```
```bash title="CLI Tool (global)"
dotnet tool install --global TurboMediator.Cli
```
Project Setup [#project-setup]
After installing the packages, register TurboMediator in your DI container:
```csharp title="Program.cs"
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Basic registration - auto-discovers all handlers
builder.Services.AddTurboMediator();
// Or with configuration
builder.Services.AddTurboMediator(mediator =>
{
mediator
.WithSequentialNotifications();
});
var app = builder.Build();
```
The source generator automatically generates the `AddTurboMediator()` extension method with all discovered handler registrations.
# Quick Start
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
This guide walks you through building a simple application with TurboMediator.
Install the packages [#install-the-packages]
```bash title="Terminal"
dotnet add package TurboMediator.Abstractions
dotnet add package TurboMediator.SourceGenerator
```
Define your messages [#define-your-messages]
TurboMediator supports four message types following CQRS principles:
```csharp title="Messages.cs"
using TurboMediator;
// Query — read operation, returns data
public record GetUserQuery(Guid Id) : IQuery;
// Command — write operation, returns result
public record CreateUserCommand(string Name, string Email) : ICommand;
// Request — generic request with response
public record PingRequest : IRequest;
// Notification — fire-and-forget, multiple handlers
public record UserCreatedNotification(Guid UserId, string Email) : INotification;
// Supporting types
public record User(Guid Id, string Name, string Email);
```
Create handlers [#create-handlers]
Each message type has a corresponding handler interface:
```csharp title="Handlers.cs"
using TurboMediator;
public class GetUserHandler : IQueryHandler
{
public ValueTask Handle(GetUserQuery query, CancellationToken ct)
{
var user = new User(query.Id, "John Doe", "john@example.com");
return new ValueTask(user);
}
}
public class CreateUserHandler : ICommandHandler
{
public ValueTask Handle(CreateUserCommand command, CancellationToken ct)
{
var user = new User(Guid.NewGuid(), command.Name, command.Email);
return new ValueTask(user);
}
}
public class PingHandler : IRequestHandler
{
public ValueTask Handle(PingRequest request, CancellationToken ct)
{
return new ValueTask("Pong!");
}
}
// Multiple notification handlers are allowed
public class EmailNotificationHandler : INotificationHandler
{
public ValueTask Handle(UserCreatedNotification notification, CancellationToken ct)
{
Console.WriteLine($"Sending welcome email to {notification.Email}");
return default;
}
}
public class LogNotificationHandler : INotificationHandler
{
public ValueTask Handle(UserCreatedNotification notification, CancellationToken ct)
{
Console.WriteLine($"User created: {notification.UserId}");
return default;
}
}
```
Register services [#register-services]
```csharp title="Program.cs"
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Register TurboMediator — handlers are auto-discovered
builder.Services.AddTurboMediator();
var app = builder.Build();
```
Use the mediator [#use-the-mediator]
Inject `IMediator` (or `ISender` / `IPublisher`) and send messages:
```csharp title="Usage"
app.MapGet("/ping", async (IMediator mediator) =>
{
var result = await mediator.Send(new PingRequest());
return Results.Ok(result); // "Pong!"
});
app.MapGet("/users/{id}", async (Guid id, IMediator mediator) =>
{
var user = await mediator.Send(new GetUserQuery(id));
return user is not null ? Results.Ok(user) : Results.NotFound();
});
app.MapPost("/users", async (CreateUserCommand command, IMediator mediator) =>
{
var user = await mediator.Send(command);
// Publish notification — all handlers will be invoked
await mediator.Publish(new UserCreatedNotification(user.Id, user.Email));
return Results.Created($"/users/{user.Id}", user);
});
app.Run();
```
The source generator automatically discovers all handlers in your project and generates compile-time dispatch code. You don't need to manually register each handler.
Compile-Time Safety [#compile-time-safety]
TurboMediator validates your handlers at build time. You'll get compiler errors for:
| Diagnostic | Description |
| ---------- | ------------------------------------------------------------- |
| `TURBO001` | Missing handler for a message |
| `TURBO002` | Multiple handlers for the same message (except notifications) |
| `TURBO003` | Invalid handler signature |
| `TURBO004` | Response type mismatch |
| `TURBO008` | Handler must be public |
What's Next? [#whats-next]
Now that you have a working setup, explore the features:
* [Messages & Handlers](/docs/core/messages) — Learn about all message types
* [Pipeline Behaviors](/docs/pipeline/behaviors) — Add cross-cutting concerns
* [Streaming](/docs/core/streaming) — Use `IAsyncEnumerable` for streaming
* [Notifications](/docs/core/notifications) — Multi-handler publish strategies
# Source Generator
import { Callout } from 'fumadocs-ui/components/callout';
TurboMediator uses a **Roslyn Incremental Source Generator** to produce compile-time optimized code. This is the core of the library's performance advantage.
What Gets Generated [#what-gets-generated]
The source generator produces:
1. **`Mediator` class** — The concrete `IMediator` implementation with `switch`-expression dispatch for each message type
2. **`ExecuteWithPipeline` method** — Resolves pre-processors, pipeline behaviors, exception handlers, and post-processors from DI and chains them
3. **`AddTurboMediator()` extension** — DI registration that wires all discovered handlers
Example Generated Code [#example-generated-code]
For a project with `PingRequest` and `GetUserQuery`:
```csharp
// Generated by TurboMediator Source Generator
internal sealed class Mediator : IMediator
{
private readonly IServiceProvider _serviceProvider;
public Mediator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public ValueTask Send(IRequest request, CancellationToken ct)
{
return request switch
{
PingRequest msg => ExecuteWithPipeline(msg, ct),
_ => throw new InvalidOperationException($"No handler for {request.GetType().Name}")
};
}
public ValueTask Send(IQuery query, CancellationToken ct)
{
return query switch
{
GetUserQuery msg => ExecuteWithPipeline(msg, ct),
_ => throw new InvalidOperationException($"No handler for {query.GetType().Name}")
};
}
// ... similar for Commands and Notifications
}
```
No Reflection [#no-reflection]
The key benefit is that dispatch is a **compile-time `switch` expression** — no `Dictionary` lookups, no `Activator.CreateInstance`, no `MethodInfo.Invoke`. This makes it:
* Faster than reflection-based mediators
* Compatible with **Native AOT** publishing
* Fully **trimmable** by the .NET linker
Build-Time Diagnostics [#build-time-diagnostics]
The source generator emits diagnostics during compilation:
| Code | Severity | Description |
| ---------- | -------- | -------------------------------------------------- |
| `TURBO001` | Warning | No handler found for message type |
| `TURBO002` | Error | Multiple handlers for the same message type |
| `TURBO003` | Error | Invalid handler signature |
| `TURBO004` | Error | Response type mismatch between message and handler |
| `TURBO005` | Warning | Handler class is abstract (cannot be instantiated) |
| `TURBO006` | Warning | No stream handler found for stream message |
| `TURBO007` | Error | Multiple stream handlers for the same message |
| `TURBO008` | Error | Handler class must be public |
| `TURBO009` | Info | Duplicate notification handler detected |
Example Build Errors [#example-build-errors]
```
error TURBO002: Multiple handlers found for 'GetUserQuery': GetUserHandler, AnotherGetUserHandler
error TURBO008: Handler 'InternalHandler' must be public to be discovered by TurboMediator
warning TURBO001: No handler found for message 'OrphanedCommand'
```
How It Works Internally [#how-it-works-internally]
1. **Parser Phase**: Scans the syntax tree for:
* Classes implementing handler interfaces (`IRequestHandler`, `ICommandHandler`, `IQueryHandler`, `INotificationHandler`, streaming variants)
* Types implementing message interfaces (`IRequest`, `ICommand`, `IQuery`, `INotification`, streaming variants)
2. **Validation Phase**: Checks for:
* Missing handlers
* Duplicate handlers
* Signature mismatches
* Accessibility (public)
3. **Emission Phase**: Generates:
* `Mediator` class with switch-expression routing
* Pipeline execution with DI-resolved behaviors
* `AddTurboMediator()` service collection extension
# Correlation ID
The correlation ID behavior ensures every operation has a unique identifier that flows through the entire pipeline and can be propagated to downstream services.
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithCorrelationId(options =>
{
options.HeaderName = "X-Correlation-ID";
options.GenerateIfMissing = true;
options.AddToActivityBaggage = true;
options.AddToLogScope = true;
options.PropagateToHttpClient = true;
});
m.WithMediatorContext(); // Required for context storage
});
```
CorrelationOptions [#correlationoptions]
| Option | Default | Description |
| ------------------------ | -------------------- | ------------------------------------ |
| `HeaderName` | `"X-Correlation-ID"` | HTTP header name |
| `GenerateIfMissing` | `true` | Auto-generate if not provided |
| `CorrelationIdGenerator` | `Guid.NewGuid()` | Custom ID generator function |
| `AddToActivityBaggage` | `true` | Add to `Activity.Current.Baggage` |
| `AddToLogScope` | `true` | Add to `ILogger` scope |
| `PropagateToHttpClient` | `true` | Add to outgoing HTTP headers |
| `CorrelationIdProvider` | `null` | Custom provider to read existing IDs |
Reading the Correlation ID [#reading-the-correlation-id]
Access through `IMediatorContextAccessor`:
```csharp
public class MyHandler : ICommandHandler
{
private readonly IMediatorContextAccessor _contextAccessor;
private readonly ILogger _logger;
public MyHandler(
IMediatorContextAccessor contextAccessor,
ILogger logger)
{
_contextAccessor = contextAccessor;
_logger = logger;
}
public async ValueTask Handle(MyCommand command, CancellationToken ct)
{
var correlationId = _contextAccessor.Context?.CorrelationId;
_logger.LogInformation("Processing with correlation ID: {CorrelationId}", correlationId);
// The correlation ID is automatically included in log scope
// and Activity baggage
return new MyResult();
}
}
```
Propagation to Downstream Services [#propagation-to-downstream-services]
When `PropagateToHttpClient` is enabled, the correlation ID is automatically added to outgoing HTTP requests via the configured header:
```csharp
// The correlation ID flows automatically to downstream HTTP calls
public class OrderHandler : ICommandHandler
{
private readonly HttpClient _httpClient;
public OrderHandler(HttpClient httpClient) => _httpClient = httpClient;
public async ValueTask Handle(CreateOrderCommand cmd, CancellationToken ct)
{
// X-Correlation-ID header is automatically added
var inventory = await _httpClient.GetFromJsonAsync(
$"/api/inventory/{cmd.ProductId}", ct);
return new Order { /* ... */ };
}
}
```
# Health Checks
TurboMediator integrates with ASP.NET Core health checks to monitor the health of the mediator system.
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithHealthCheck(options =>
{
options.Tags = new[] { "ready" };
options.CheckHandlerRegistration = true;
options.CheckCircuitBreakers = true;
options.CheckSagaStore = true;
options.CheckOutboxBacklog = true;
options.MaxOutboxBacklog = 1000;
options.Timeout = TimeSpan.FromSeconds(5);
});
});
// Map health endpoint
app.MapHealthChecks("/health");
```
TurboMediatorHealthCheckOptions [#turbomediatorhealthcheckoptions]
| Option | Default | Description |
| -------------------------- | ---------------------------- | ------------------------------------------------- |
| `CheckHandlerRegistration` | `true` | Verify all message types have registered handlers |
| `CheckCircuitBreakers` | `true` | Report unhealthy if any circuit breaker is open |
| `CheckSagaStore` | `true` | Check saga store connectivity |
| `CheckOutboxBacklog` | `true` | Check outbox backlog size |
| `MaxOutboxBacklog` | `1000` | Maximum outbox backlog before reporting degraded |
| `DegradedThreshold` | `0.8` | Percentage of MaxOutboxBacklog before degraded |
| `Timeout` | `5s` | Timeout for health check operations |
| `Tags` | `["turbomediator", "ready"]` | Tags for filtering health checks |
| `IncludeDetails` | `true` | Include detailed information in response |
Health Check Result [#health-check-result]
The health check verifies:
* Mediator is registered and can be resolved from DI
* Handler resolution works correctly
* Pipeline is configured properly
Returns `Healthy`, `Degraded`, or `Unhealthy` based on the check results.
# Metrics
The metrics behavior provides detailed request-level metrics using `System.Diagnostics.Metrics`, independent of the telemetry behavior.
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithMetrics(options =>
{
options.MeterName = "MyApp.Mediator";
options.MeterVersion = "1.0.0";
});
});
```
MetricsOptions [#metricsoptions]
| Option | Default | Description |
| --------------------------- | ----------------- | ---------------------------------------- |
| `EnableLatencyHistogram` | `true` | Record request duration histogram |
| `EnableThroughputCounter` | `true` | Count total requests |
| `EnableErrorCounter` | `true` | Count errors |
| `EnableInFlightGauge` | `true` | Track in-flight requests |
| `LatencyBuckets` | Default | Custom histogram buckets |
| `CustomLabels` | `[]` | Additional label names (`IList`) |
| `CustomLabelValuesProvider` | `null` | Provider for custom label values |
| `MeterName` | `"TurboMediator"` | Meter name for filtering |
| `MeterVersion` | `"1.0.0"` | Meter version |
Instruments Created [#instruments-created]
| Instrument | Type | Description |
| -------------------------------- | ------------- | ------------------------------- |
| `turbomediator.handler.duration` | Histogram | Handler execution duration (ms) |
| `turbomediator.handler.requests` | Counter | Total handler invocations |
| `turbomediator.handler.errors` | Counter | Handler errors |
| `turbomediator.handler.inflight` | UpDownCounter | Currently executing handlers |
All metrics include `message_type` as a label.
Practical Example [#practical-example]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithMetrics(options =>
{
options.LatencyBuckets = new double[] { 5, 10, 25, 50, 100, 250, 500, 1000 };
options.CustomLabels = new List { "service", "environment" };
options.CustomLabelValuesProvider = () => new Dictionary
{
["service"] = "order-service",
["environment"] = builder.Environment.EnvironmentName
};
});
});
// Export to Prometheus
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics
.AddMeter("TurboMediator")
.AddPrometheusExporter();
});
```
# Structured Logging
The structured logging behavior provides detailed, structured logs for every mediator operation with support for sensitive property masking and slow operation detection.
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithStructuredLogging(options =>
{
options.IncludeMessageType = true;
options.IncludeHandlerName = true;
options.IncludeDuration = true;
options.IncludeCorrelationId = true;
options.IncludeMessageProperties = true;
options.IncludeResponse = false;
options.SlowOperationThreshold = TimeSpan.FromSeconds(1);
options.SensitivePropertyNames = new HashSet
{
"Password", "CreditCard", "SSN", "Token"
};
});
});
```
StructuredLoggingOptions [#structuredloggingoptions]
| Option | Default | Description |
| -------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `IncludeMessageType` | `true` | Log the message type name |
| `IncludeHandlerName` | `true` | Log the handler class name |
| `IncludeDuration` | `true` | Log execution duration |
| `IncludeCorrelationId` | `true` | Log correlation ID |
| `IncludeMessageProperties` | `false` | Log message property values |
| `IncludeResponse` | `false` | Log response value |
| `SensitivePropertyNames` | Password, Secret, Token, ApiKey, ConnectionString, CreditCard, CardNumber, Cvv, Pin, Ssn, SocialSecurityNumber | Properties to mask in logs |
| `SuccessLogLevel` | `Information` | Log level for successes |
| `ErrorLogLevel` | `Error` | Log level for errors |
| `SlowOperationLogLevel` | `Warning` | Log level for slow operations |
| `SlowOperationThreshold` | 1 second | Duration to be considered slow |
| `ShouldLog` | `null` | Predicate to skip logging for certain messages |
Sensitive Property Masking [#sensitive-property-masking]
Properties listed in `SensitivePropertyNames` are replaced with `***REDACTED***` in log output:
```csharp
// Configuration
options.SensitivePropertyNames = new HashSet { "Password", "Token" };
// Message
public record LoginCommand(string Username, string Password) : ICommand;
// Log output:
// Handling LoginCommand: { Username = "alice", Password = "***REDACTED***" }
```
Slow Operation Detection [#slow-operation-detection]
Operations exceeding `SlowOperationThreshold` are logged at a higher level:
```
[Warning] Slow handler detected: GenerateReportQuery took 2345ms (threshold: 1000ms)
```
Log Output Examples [#log-output-examples]
```
[Information] Handling CreateUserCommand:
MessageType: CreateUserCommand
CorrelationId: abc-123
Properties: { Name = "Alice", Email = "alice@example.com" }
[Information] Handled CreateUserCommand in 45ms
HandlerName: CreateUserHandler
Duration: 45ms
[Error] Error handling CreateUserCommand after 12ms
HandlerName: CreateUserHandler
Exception: System.InvalidOperationException: Email already exists
```
Conditional Logging [#conditional-logging]
Skip logging for specific message types:
```csharp
options.ShouldLog = (messageType) =>
{
// Don't log health check queries
return messageType != typeof(HealthCheckQuery);
};
```
# Telemetry
import { Callout } from 'fumadocs-ui/components/callout';
The telemetry behavior integrates with OpenTelemetry to provide distributed tracing and metrics for all mediator operations.
Installation [#installation]
```bash
dotnet add package TurboMediator.Observability
```
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
// Apply to all messages
m.WithTelemetry(options =>
{
options.RecordTraces = true;
options.RecordMetrics = true;
options.RecordExceptionStackTrace = false;
});
// Or apply to a specific message type
m.WithTelemetry(options =>
{
options.RecordExceptionStackTrace = true;
});
});
```
TelemetryOptions [#telemetryoptions]
| Option | Default | Description |
| --------------------------- | ------- | ---------------------------------------- |
| `RecordTraces` | `true` | Create Activity spans for each operation |
| `RecordMetrics` | `true` | Record counters and histograms |
| `RecordExceptionStackTrace` | `false` | Include stack traces in error events |
Built-in Metrics [#built-in-metrics]
`TurboMediatorTelemetry` provides these static instruments:
| Metric | Type | Description |
| ----------------------------------- | --------- | -------------------------------- |
| `turbomediator.requests.total` | Counter | Total requests processed |
| `turbomediator.requests.success` | Counter | Successful requests |
| `turbomediator.requests.failure` | Counter | Failed requests |
| `turbomediator.requests.duration` | Histogram | Request duration in milliseconds |
| `turbomediator.notifications.total` | Counter | Total notifications published |
Custom Telemetry Tags [#custom-telemetry-tags]
Implement `ITelemetryEnriched` on your messages to add custom tags to traces:
```csharp
public record ProcessOrderCommand(Guid OrderId, string Region)
: ICommand, ITelemetryEnriched
{
public IEnumerable> GetTelemetryTags()
{
return new Dictionary
{
["order.id"] = OrderId.ToString(),
["order.region"] = Region
};
}
}
```
OpenTelemetry Integration [#opentelemetry-integration]
Register the TurboMediator source with your OpenTelemetry configuration:
```csharp
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddSource(TurboMediatorTelemetry.ActivitySource.Name)
.AddAspNetCoreInstrumentation()
.AddOtlpExporter();
})
.WithMetrics(metrics =>
{
metrics
.AddMeter(TurboMediatorTelemetry.Meter.Name)
.AddOtlpExporter();
});
```
Activity Spans [#activity-spans]
Each mediator operation creates an `Activity` span with:
* **Name**: Message type name (e.g., `CreateUserCommand`)
* **Tags**: Message type, handler type, success/failure status, duration
* **Events**: Exception details on failure
* **Custom tags**: From `ITelemetryEnriched` implementations
The telemetry behavior automatically correlates with ASP.NET Core and HttpClient traces when using OpenTelemetry, giving you end-to-end distributed tracing.
# Batching
import { Callout } from 'fumadocs-ui/components/callout';
The batching behavior collects multiple individual queries and processes them together in a single batch, significantly reducing database round-trips and improving throughput.
Installation [#installation]
```bash
dotnet add package TurboMediator.Batching
```
Defining Batchable Queries [#defining-batchable-queries]
Mark queries with `IBatchableQuery`:
```csharp
public record GetProductByIdQuery(Guid ProductId)
: IBatchableQuery;
public record GetUserByIdQuery(Guid UserId)
: IBatchableQuery;
```
Creating Batch Handlers [#creating-batch-handlers]
Implement `IBatchHandler` to handle a batch of queries at once:
```csharp
public class GetProductBatchHandler
: IBatchHandler
{
private readonly AppDbContext _db;
public GetProductBatchHandler(AppDbContext db) => _db = db;
public async ValueTask> HandleAsync(
IReadOnlyList queries,
CancellationToken ct)
{
var ids = queries.Select(q => q.ProductId).ToList();
// Single database query for all products
var products = await _db.Products
.Where(p => ids.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, ct);
return queries.ToDictionary(
q => q,
q => products.GetValueOrDefault(q.ProductId)
);
}
}
```
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithBatching(batching =>
{
batching
.WithMaxBatchSize(50)
.WithMaxWaitTime(TimeSpan.FromMilliseconds(20))
.WithThrowIfNoBatchHandler(false)
.OnBatchProcessed(info =>
{
Console.WriteLine($"Batch of {info.BatchSize} {info.QueryType.Name} processed in {info.Duration.TotalMilliseconds}ms");
});
});
});
```
BatchingOptions [#batchingoptions]
| Option | Default | Description |
| ----------------------- | ------- | ----------------------------------------------------------- |
| `MaxBatchSize` | 100 | Maximum queries per batch |
| `MaxWaitTime` | 10ms | Maximum time to wait for batch to fill |
| `ThrowIfNoBatchHandler` | `false` | Throw if no batch handler exists (falls back to individual) |
| `OnBatchProcessed` | `null` | Callback after batch processing |
How It Works [#how-it-works]
1. Individual `Send()` calls with `IBatchableQuery` are queued
2. The batching behavior collects queries in a `ConcurrentQueue`
3. After `MaxWaitTime` or reaching `MaxBatchSize`, the batch is processed
4. Each query receives its individual result from the batch
5. If no `IBatchHandler` is registered, queries fall back to individual execution
Practical Example [#practical-example]
```csharp
// API endpoint that triggers many product lookups
app.MapPost("/cart/validate", async (
CartRequest cart,
IMediator mediator) =>
{
// These queries are automatically batched into a single DB call
var tasks = cart.Items.Select(async item =>
{
var product = await mediator.Send(new GetProductByIdQuery(item.ProductId));
return new ValidatedItem(item.ProductId, product is not null, product?.Price);
});
var results = await Task.WhenAll(tasks);
return Results.Ok(results);
});
```
Batching is most effective when many queries of the same type are sent concurrently, such as in GraphQL resolvers or bulk validation scenarios. The behavior transparently groups queries without requiring any changes to the caller.
# Caching
import { Callout } from 'fumadocs-ui/components/callout';
The `TurboMediator.Caching` package provides automatic handler response caching based on attributes, avoiding redundant processing for the same input.
Installation [#installation]
```bash
dotnet add package TurboMediator.Caching
```
Using the Attribute [#using-the-attribute]
```csharp
[Cacheable(durationSeconds: 300)] // Cache for 5 minutes
public record GetProductQuery(Guid Id) : IQuery;
[Cacheable(durationSeconds: 60, UseSlidingExpiration = true)]
public record GetConfigQuery(string Key) : IQuery;
[Cacheable(durationSeconds: 600, KeyPrefix = "catalog")]
public record GetCatalogQuery(string Category) : IQuery;
```
Attribute Properties [#attribute-properties]
| Property | Description |
| ---------------------- | ------------------------------- |
| `durationSeconds` | Cache duration |
| `KeyPrefix` | Optional prefix for cache keys |
| `UseSlidingExpiration` | Reset expiration on each access |
Cache Key Generation [#cache-key-generation]
By default, cache keys are generated using a **SHA256 hash** of the serialized message. For custom keys, implement `ICacheKeyProvider`:
```csharp
public record GetUserQuery(Guid Id) : IQuery, ICacheKeyProvider
{
public string GetCacheKey() => $"user:{Id}";
}
```
Cache Providers [#cache-providers]
In-Memory (Built-in) [#in-memory-built-in]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithCaching, object>();
m.WithInMemoryCache();
});
```
Redis (Official Package) [#redis-official-package]
Install the Redis provider:
```bash
dotnet add package TurboMediator.Caching.Redis
```
Register with a connection string:
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithCaching, object>();
m.WithRedisCache("localhost:6379");
});
```
Or configure with full options:
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithCaching, object>();
m.WithRedisCache(options =>
{
options.ConnectionString = "localhost:6379";
options.Database = 1;
options.KeyPrefix = "myapp";
options.SerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
});
});
```
You can also reuse an existing `IConnectionMultiplexer`:
```csharp
var redis = ConnectionMultiplexer.Connect("localhost:6379");
builder.Services.AddSingleton(redis);
builder.Services.AddTurboMediator(m =>
{
m.WithRedisCache(redis, options =>
{
options.KeyPrefix = "myapp";
});
});
```
Redis Features [#redis-features]
| Feature | Description |
| ------------------------ | ------------------------------------------------------------------------ |
| **Key Prefix** | Isolate cache entries per application with `KeyPrefix` |
| **Sliding Expiration** | Automatically renews TTL on each cache access |
| **Database Selection** | Target a specific Redis database number |
| **Custom Serialization** | Configure `JsonSerializerOptions` for serialization |
| **Connection Reuse** | Pass an existing `IConnectionMultiplexer` to avoid duplicate connections |
Custom Provider [#custom-provider]
Implement `ICacheProvider`:
```csharp
public interface ICacheProvider
{
ValueTask> GetAsync(string key, CancellationToken cancellationToken = default);
ValueTask SetAsync(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default);
ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);
}
```
Register it:
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithCacheProvider();
});
```
Practical Example [#practical-example]
```csharp
// Cacheable query
[Cacheable(durationSeconds: 120, KeyPrefix = "products")]
public record GetPopularProductsQuery(string Category, int Limit)
: IQuery>, ICacheKeyProvider
{
public string GetCacheKey() => $"popular:{Category}:{Limit}";
}
// Handler — only called on cache miss
public class GetPopularProductsHandler
: IQueryHandler>
{
private readonly IProductRepository _repo;
public GetPopularProductsHandler(IProductRepository repo) => _repo = repo;
public async ValueTask> Handle(
GetPopularProductsQuery query, CancellationToken ct)
{
return await _repo.GetPopularByCategory(query.Category, query.Limit, ct);
}
}
// Cache invalidation after write
public class UpdateProductHandler : ICommandHandler
{
private readonly ICacheProvider _cache;
public UpdateProductHandler(ICacheProvider cache) => _cache = cache;
public async ValueTask Handle(UpdateProductCommand command, CancellationToken ct)
{
// Update product...
// Invalidate cache
await _cache.RemoveAsync($"products:popular:{command.Category}:{10}");
return Unit.Value;
}
}
```
Caching is most effective for queries (read operations). Avoid caching commands that have side effects.
# Distributed Locking
import { Callout } from 'fumadocs-ui/components/callout';
Distributed locking ensures that only one instance of your application processes a given operation at a time — across pods, containers, or servers. TurboMediator integrates this as a pipeline behavior, keeping your handlers free of lock management boilerplate.
Distributed locking is a **concurrency** concern distinct from [Deduplication](/docs/enterprise/deduplication).
Deduplication prevents the same message from being processed **twice** (idempotency).
Distributed locking prevents the same **resource** from being processed **concurrently** by different instances.
Packages [#packages]
| Package | Purpose |
| ---------------------------------------- | ------------------------------------------------------------------------------------------ |
| `TurboMediator.DistributedLocking` | Core behavior, abstractions, in-memory provider |
| `TurboMediator.DistributedLocking.Redis` | Redis provider via [madelson/DistributedLock](https://github.com/madelson/DistributedLock) |
Installation [#installation]
```bash
# Core (required)
dotnet add package TurboMediator.DistributedLocking
# Redis provider (production)
dotnet add package TurboMediator.DistributedLocking.Redis
```
Basic Usage [#basic-usage]
1. Decorate your command [#1-decorate-your-command]
```csharp
[DistributedLock]
public record WithdrawCommand(Guid AccountId, decimal Amount)
: ICommand, ILockKeyProvider
{
// Lock is scoped to this specific account — multiple accounts run concurrently.
public string GetLockKey() => AccountId.ToString();
}
```
2. Register the behavior [#2-register-the-behavior]
```csharp
builder.Services.AddTurboMediator(m => m
// Lock provider
.WithInMemoryDistributedLocking() // development / single-node
// .WithRedisDistributedLocking("localhost:6379") // production
// Register the behavior for this command
.WithDistributedLocking()
);
```
That's it. The handler runs only after the lock is acquired, and the lock is released automatically when the handler completes — even on exceptions.
Lock Key Strategies [#lock-key-strategies]
Per-entity lock (recommended) [#per-entity-lock-recommended]
Implement `ILockKeyProvider` to scope the lock to a single entity, allowing unrelated entities to be processed concurrently:
```csharp
[DistributedLock]
public record TransferFundsCommand(Guid FromAccountId, Guid ToAccountId, decimal Amount)
: ICommand, ILockKeyProvider
{
// Only one transfer at a time per source account
public string GetLockKey() => FromAccountId.ToString();
}
```
Global message-type lock [#global-message-type-lock]
Without `ILockKeyProvider`, the lock key defaults to the message type name — only one instance of this command type runs across the cluster at any given time:
```csharp
[DistributedLock(TimeoutSeconds = 60)]
public record RunDailyReportCommand : ICommand;
// Lock key → "RunDailyReportCommand" (global for this command type)
```
Composite keys [#composite-keys]
Combine multiple properties for finer granularity:
```csharp
[DistributedLock(KeyPrefix = "invoice")]
public record ApproveInvoiceCommand(Guid TenantId, Guid InvoiceId)
: ICommand, ILockKeyProvider
{
public string GetLockKey() => $"{TenantId}:{InvoiceId}";
// Resulting key → "invoice:tenant-abc:invoice-123"
}
```
Attribute Options [#attribute-options]
```csharp
[DistributedLock(
KeyPrefix = "payment",
TimeoutSeconds = 15,
ThrowIfNotAcquired = true
)]
public record ProcessPaymentCommand(Guid PaymentId) : ICommand, ILockKeyProvider
{
public string GetLockKey() => PaymentId.ToString();
}
```
| Property | Default | Description |
| -------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
| `KeyPrefix` | *type name* | Prefix for the lock key. Defaults to `typeof(TMessage).Name`. |
| `TimeoutSeconds` | `30` | Seconds to wait for the lock before failing. Set to `0` to fail immediately. |
| `ThrowIfNotAcquired` | `true` | Throw `DistributedLockException` if the lock cannot be acquired. When `false`, returns `default(TResponse)` instead. |
Global Options [#global-options]
Override defaults for all locking behaviors at registration time:
```csharp
builder.Services.AddTurboMediator(m => m
.WithInMemoryDistributedLocking()
.WithDistributedLocking(options =>
{
options.DefaultTimeout = TimeSpan.FromSeconds(10);
options.GlobalKeyPrefix = "myapp"; // → "myapp:WithdrawCommand:accountId"
options.DefaultThrowIfNotAcquired = true;
})
);
```
Providers [#providers]
In-Memory (development / single-node) [#in-memory-development--single-node]
Uses `SemaphoreSlim` per key. Zero dependencies, works without any external infrastructure. Not suitable for multi-instance deployments.
```csharp
builder.Services.AddTurboMediator(m => m
.WithInMemoryDistributedLocking()
// ...
);
```
Redis (production) [#redis-production]
Backed by [madelson/DistributedLock](https://github.com/madelson/DistributedLock), which uses the [RedLock](https://redis.io/docs/manual/patterns/distributed-locks/) algorithm under the hood.
**Using a connection string (standalone setup):**
```csharp
builder.Services.AddTurboMediator(m => m
.WithRedisDistributedLocking("localhost:6379,abortConnect=false")
// ...
);
```
**Full configuration:**
```csharp
builder.Services.AddTurboMediator(m => m
.WithRedisDistributedLocking(options =>
{
options.ConnectionString = "redis-primary:6379,redis-replica:6380";
options.Database = 0;
options.KeyPrefix = "turbo:locks";
})
// ...
);
```
**Reusing an existing `IConnectionMultiplexer` from DI:**
```csharp
// Already registered elsewhere:
builder.Services.AddSingleton(
ConnectionMultiplexer.Connect("localhost:6379"));
builder.Services.AddTurboMediator(m => m
.WithRedisDistributedLockingFromDI(options => options.KeyPrefix = "locks")
// ...
);
```
Custom Provider [#custom-provider]
Implement `IDistributedLockProvider` for any backend (SQL Server, Azure Blob, etcd, …):
```csharp
public class SqlDistributedLockProvider : IDistributedLockProvider
{
public async Task TryAcquireAsync(
string key, TimeSpan timeout, CancellationToken cancellationToken)
{
// your implementation
}
}
// Register:
builder.Services.AddTurboMediator(m => m
.WithDistributedLockProvider()
// ...
);
```
Error Handling [#error-handling]
When `ThrowIfNotAcquired = true` (default), a `DistributedLockException` is thrown if the lock times out:
```csharp
try
{
var result = await mediator.Send(new WithdrawCommand(accountId, amount));
}
catch (DistributedLockException ex)
{
// ex.LockKey → the key that could not be acquired
// ex.Timeout → the timeout that was exhausted
logger.LogWarning("Lock contention: {Key} waited {Timeout}s", ex.LockKey, ex.Timeout.TotalSeconds);
return Conflict("This account is currently locked. Please retry.");
}
```
When `ThrowIfNotAcquired = false`, `default(TResponse)` is returned and the handler is **not** called. Use this only when skipping the operation on contention is acceptable:
```csharp
[DistributedLock(ThrowIfNotAcquired = false, TimeoutSeconds = 0)]
public record BackgroundSyncCommand(Guid TenantId) : ICommand, ILockKeyProvider
{
public string GetLockKey() => TenantId.ToString();
}
// Returns null instead of throwing if another sync is already running for this tenant.
```
Testing [#testing]
The `InMemoryDistributedLockProvider` works perfectly in unit tests without any mocking:
```csharp
builder.Services.AddTurboMediator(m => m
.WithInMemoryDistributedLocking()
.WithDistributedLocking()
);
```
You can also register a no-op stub if you want to isolate handler logic from locking behavior:
```csharp
public class NoOpLockProvider : IDistributedLockProvider
{
private sealed class AlwaysAcquiredHandle : IDistributedLockHandle
{
public string Key { get; init; } = string.Empty;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
public Task TryAcquireAsync(
string key, TimeSpan timeout, CancellationToken ct)
=> Task.FromResult(new AlwaysAcquiredHandle { Key = key });
}
```
When to Use Distributed Locking [#when-to-use-distributed-locking]
| Scenario | Why it helps |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| Account balance operations (withdraw, transfer) | Prevents double-spending from concurrent requests |
| Inventory reservation | Prevents over-selling the same stock unit |
| Scheduled job execution | Ensures only one instance runs a job at a time |
| Saga / saga step execution | Prevents two instances from advancing the same saga concurrently |
| External API calls with side effects | Avoids duplicated charges or state changes |
Distributed locking introduces latency and a dependency on an external coordination service.
Use it only for **write operations** on shared mutable state. Read-only queries should never need a lock.
Sample [#sample]
See [`samples/Sample.DistributedLocking`](https://github.com/marcocestari/TurboMediator/tree/main/samples/Sample.DistributedLocking) for a complete bank account API demonstrating per-account locking, concurrent stress-testing, and lock contention handling.
# Feature Flags
import { Callout } from 'fumadocs-ui/components/callout';
The feature flag behavior allows you to conditionally enable or disable message handlers based on feature flags, without deploying new code.
Installation [#installation]
```bash
dotnet add package TurboMediator.FeatureFlags
# Optional: Microsoft.FeatureManagement integration
dotnet add package TurboMediator.FeatureFlags.FeatureManagement
```
Using the Attribute [#using-the-attribute]
```csharp
[FeatureFlag("new-checkout")]
public record NewCheckoutCommand(Guid CartId) : ICommand;
[FeatureFlag("beta-search", FallbackBehavior = FeatureFallback.ReturnDefault)]
public record BetaSearchQuery(string Term) : IQuery;
[FeatureFlag("premium-export", FallbackBehavior = FeatureFallback.Throw)]
public record ExportReportCommand(Guid ReportId) : ICommand;
[FeatureFlag("per-user-feature", PerUser = true)]
public record PersonalizedFeedQuery : IQuery;
```
Attribute Properties [#attribute-properties]
| Property | Default | Description |
| ------------------ | -------- | ------------------------------------------------------------ |
| Feature name | Required | The feature flag name |
| `FallbackBehavior` | `Throw` | What to do when flag is disabled: Throw, ReturnDefault, Skip |
| `PerUser` | `false` | Evaluate per-user instead of globally |
IFeatureFlagProvider [#ifeatureflagprovider]
```csharp
public interface IFeatureFlagProvider
{
ValueTask IsEnabledAsync(string featureName, CancellationToken cancellationToken = default);
ValueTask IsEnabledAsync(string featureName, string userId, CancellationToken cancellationToken = default);
}
```
In-Memory Provider [#in-memory-provider]
For development and testing:
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithFeatureFlags(ff =>
{
ff.UseInMemoryProvider()
.WithFeature("new-checkout", true)
.WithFeature("beta-search", false)
.WithFeature("premium-export", enabled: true);
});
});
```
Microsoft.FeatureManagement Integration [#microsoftfeaturemanagement-integration]
Integrate with `Microsoft.FeatureManagement` for configuration-driven flags:
```bash
dotnet add package TurboMediator.FeatureFlags.FeatureManagement
```
```csharp
// appsettings.json
{
"FeatureManagement": {
"new-checkout": true,
"beta-search": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": { "Value": 50 }
}
]
}
}
}
// Registration
builder.Services.AddFeatureManagement();
builder.Services.AddMicrosoftFeatureFlags();
builder.Services.AddTurboMediator(m =>
{
m.WithFeatureFlags(ff =>
{
// FeatureManagement provider is auto-detected
});
});
```
FeatureFlagOptions [#featureflagoptions]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithFeatureFlags(ff =>
{
ff.UseInMemoryProvider()
.UseStrictMode() // or .UseLenientMode()
.WithUserIdProvider(() => currentUserService.GetUserId());
});
});
```
| Option | Description |
| ----------------------------------- | -------------------------------------------------------- |
| `UseStrictMode()` | Sets default fallback to `FeatureFallback.Throw` |
| `UseLenientMode()` | Sets default fallback to `FeatureFallback.ReturnDefault` |
| `WithUserIdProvider(Func)` | Parameterless function to get user ID |
Custom Provider [#custom-provider]
```csharp
public class LaunchDarklyProvider : IFeatureFlagProvider
{
private readonly ILdClient _client;
public LaunchDarklyProvider(ILdClient client) => _client = client;
public ValueTask IsEnabledAsync(string featureName)
{
return new ValueTask(
_client.BoolVariation(featureName, Context.Default, false));
}
public ValueTask IsEnabledAsync(string featureName, string userId)
{
var context = Context.Builder(userId).Build();
return new ValueTask(
_client.BoolVariation(featureName, context, false));
}
}
```
Feature flags are evaluated at the pipeline level, so disabled features never reach the handler. This provides a clean way to do gradual rollouts, A/B testing, and kill switches.
# FluentValidation
import { Callout } from 'fumadocs-ui/components/callout';
The FluentValidation integration automatically validates messages before they reach the handler, using your existing FluentValidation validators.
Installation [#installation]
```bash
dotnet add package TurboMediator.FluentValidation
```
Configuration [#configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
// Auto-discover validators in the current assembly
m.WithFluentValidation();
// Or specify the assembly
m.WithFluentValidation();
});
```
Creating Validators [#creating-validators]
Define validators using standard FluentValidation:
```csharp
using FluentValidation;
public class CreateUserCommandValidator : AbstractValidator
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100).WithMessage("Name must be 100 characters or less");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
RuleFor(x => x.Age)
.InclusiveBetween(18, 120).WithMessage("Age must be between 18 and 120");
}
}
public class UpdateProductCommandValidator : AbstractValidator
{
private readonly IProductRepository _repository;
public UpdateProductCommandValidator(IProductRepository repository)
{
_repository = repository;
RuleFor(x => x.Name)
.NotEmpty()
.MustAsync(async (name, ct) =>
{
var existing = await _repository.FindByNameAsync(name, ct);
return existing is null;
}).WithMessage("Product name already exists");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be positive");
}
}
```
ValidationException [#validationexception]
When validation fails, a `ValidationException` is thrown with all failures:
```csharp
public class ValidationException : Exception
{
public IReadOnlyList Failures { get; }
}
public class ValidationFailure
{
public string PropertyName { get; }
public string ErrorMessage { get; }
public object? AttemptedValue { get; }
public string? ErrorCode { get; }
public ValidationSeverity Severity { get; }
}
public enum ValidationSeverity
{
Error,
Warning,
Info
}
```
Handling Validation Errors [#handling-validation-errors]
```csharp
app.MapPost("/users", async (CreateUserCommand command, IMediator mediator) =>
{
try
{
var user = await mediator.Send(command);
return Results.Created($"/users/{user.Id}", user);
}
catch (ValidationException ex)
{
var errors = ex.Failures
.Select(f => new { f.PropertyName, f.ErrorMessage })
.ToList();
return Results.BadRequest(new { Errors = errors });
}
});
```
Global Exception Handler [#global-exception-handler]
```csharp
app.UseExceptionHandler(app =>
{
app.Run(async context =>
{
var exception = context.Features.Get()?.Error;
if (exception is ValidationException validationEx)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new
{
Type = "ValidationError",
Errors = validationEx.Failures.Select(f => new
{
Field = f.PropertyName,
Message = f.ErrorMessage,
Severity = f.Severity.ToString()
})
});
}
});
});
```
Multiple Validators [#multiple-validators]
You can define multiple validators for the same message. All validators are executed and all failures are collected:
```csharp
// Basic validation rules
public class CreateOrderBasicValidator : AbstractValidator
{
public CreateOrderBasicValidator()
{
RuleFor(x => x.Items).NotEmpty();
}
}
// Business rule validation (separate concerns)
public class CreateOrderBusinessValidator : AbstractValidator
{
public CreateOrderBusinessValidator(IInventoryService inventory)
{
RuleForEach(x => x.Items).MustAsync(async (item, ct) =>
{
var stock = await inventory.GetStockAsync(item.ProductId, ct);
return stock >= item.Quantity;
}).WithMessage("Insufficient stock");
}
}
```
The `FluentValidationBehavior` resolves **all** `IValidator` from DI and runs them. Failures from all validators are aggregated into a single `ValidationException`.
# Rate Limiting
import { Callout } from 'fumadocs-ui/components/callout';
The rate limiting package provides two complementary behaviors: **Rate Limiting** to control request throughput, and **Bulkhead Isolation** to limit concurrent executions.
Installation [#installation]
```bash
dotnet add package TurboMediator.RateLimiting
```
Rate Limiting [#rate-limiting]
Using the Attribute [#using-the-attribute]
```csharp
[RateLimit(maxRequests: 10, windowSeconds: 60)]
public record SendSmsCommand(string Phone, string Message) : ICommand;
[RateLimit(maxRequests: 100, windowSeconds: 60, PerUser = true)]
public record SearchQuery(string Term) : IQuery;
[RateLimit(maxRequests: 50, windowSeconds: 60, PerTenant = true)]
public record ApiCallCommand(string Endpoint) : ICommand;
```
Using Configuration [#using-configuration]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithRateLimiting(options =>
{
options.MaxRequests = 10;
options.WindowSeconds = 60;
options.Algorithm = RateLimiterAlgorithm.SlidingWindow;
options.PerUser = true;
options.ThrowOnRateLimitExceeded = true;
});
// Or apply globally
m.WithGlobalRateLimiting(options =>
{
options.MaxRequests = 1000;
options.WindowSeconds = 60;
});
});
```
RateLimitOptions [#ratelimitoptions]
| Option | Default | Description |
| -------------------------- | ---------------------------------- | --------------------------------------------------------------- |
| `MaxRequests` | 100 | Max requests per window |
| `WindowSeconds` | 60 | Time window in seconds |
| `PerUser` | `false` | Per-user rate limiting |
| `PerTenant` | `false` | Per-tenant rate limiting |
| `PerIpAddress` | `false` | Per-IP rate limiting |
| `Algorithm` | `RateLimiterAlgorithm.FixedWindow` | Algorithm: FixedWindow, SlidingWindow, TokenBucket, Concurrency |
| `TokensPerPeriod` | 10 | Tokens per period (TokenBucket algorithm) |
| `SegmentsPerWindow` | 4 | Segments per window (SlidingWindow algorithm) |
| `QueueProcessingOrder` | `OldestFirst` | Order for processing queued requests |
| `PolicyName` | `null` | Named policy identifier |
| `QueueExceededRequests` | `false` | Queue instead of reject |
| `MaxQueueSize` | 0 | Maximum queue size |
| `ThrowOnRateLimitExceeded` | `true` | Throw vs return default |
| `UserIdProvider` | `null` | Custom user ID provider |
| `TenantIdProvider` | `null` | Custom tenant ID provider |
| `IpAddressProvider` | `null` | Custom IP provider |
| `CustomRateLimiterFactory` | `null` | Custom rate limiter |
Algorithms [#algorithms]
| Algorithm | Description |
| --------------- | ----------------------------------------- |
| `FixedWindow` | Fixed time windows (e.g., 100 req/min) |
| `SlidingWindow` | Rolling time window for smoother limiting |
| `TokenBucket` | Token-based with steady refill rate |
| `Concurrency` | Limits concurrent executions |
Convenience Methods [#convenience-methods]
```csharp
m.WithSlidingWindowRateLimit(
maxRequests: 50, windowSeconds: 60, segmentsPerWindow: 4);
m.WithTokenBucketRateLimit(
bucketSize: 100, tokensPerPeriod: 10, replenishmentPeriodSeconds: 60);
m.WithPerUserRateLimit(
maxRequestsPerUser: 10, windowSeconds: 60, userIdProvider: () => GetCurrentUserId());
```
RateLimitExceededException [#ratelimitexceededexception]
```csharp
try
{
await mediator.Send(new SendSmsCommand("555-1234", "Hello"));
}
catch (RateLimitExceededException ex)
{
// ex.MessageType — what was rate limited
// ex.PartitionKey — the partition that exceeded limits
// ex.RetryAfter — suggested retry delay
return Results.StatusCode(429);
}
```
***
Bulkhead Isolation [#bulkhead-isolation]
Limits **concurrent execution** of handlers to prevent resource exhaustion.
Using the Attribute [#using-the-attribute-1]
```csharp
[Bulkhead(maxConcurrent: 5, maxQueue: 10)]
public record ProcessVideoCommand(string VideoUrl) : ICommand;
```
Using Configuration [#using-configuration-1]
```csharp
builder.Services.AddTurboMediator(m =>
{
m.WithBulkhead(options =>
{
options.MaxConcurrent = 5;
options.MaxQueue = 10;
options.QueueTimeout = TimeSpan.FromSeconds(30);
options.ThrowOnBulkheadFull = true;
options.TrackMetrics = true;
options.OnRejection = (info) =>
{
Console.WriteLine($"Bulkhead full for {info.MessageType}: {info.Reason}");
};
});
// Or global bulkhead
m.WithGlobalBulkhead(options =>
{
options.MaxConcurrent = 50;
});
});
```
BulkheadOptions [#bulkheadoptions]
| Option | Default | Description |
| ---------------------- | ------- | -------------------------------- |
| `MaxConcurrent` | 10 | Maximum concurrent executions |
| `MaxQueue` | 100 | Maximum queued requests |
| `QueueTimeout` | `null` | Timeout waiting in queue |
| `ThrowOnBulkheadFull` | `true` | Throw `BulkheadFullException` |
| `TrackMetrics` | `false` | Track concurrency metrics |
| `PerPartition` | `false` | Separate bulkheads per partition |
| `PartitionKeyProvider` | `null` | Custom partition key |
| `OnRejection` | `null` | Callback when rejected |
Combined Rate Limiting + Bulkhead [#combined-rate-limiting--bulkhead]
```csharp
builder.Services.AddTurboMediator(m =>
{
// Apply both at once
m.WithThrottling(
rateLimitOptions =>
{
rateLimitOptions.MaxRequests = 100;
rateLimitOptions.WindowSeconds = 60;
},
bulkheadOptions =>
{
bulkheadOptions.MaxConcurrent = 5;
});
});
```
Rate limiting controls **throughput** (requests per time window), while bulkhead isolation controls **concurrency** (simultaneous executions). Use both together for comprehensive resource protection.
# Result Pattern
import { Callout } from 'fumadocs-ui/components/callout';
The `TurboMediator.Result` package provides `Result` and `Result` types for explicit error handling without relying on exceptions for control flow.
Installation [#installation]
```bash
dotnet add package TurboMediator.Result
```
```csharp
using TurboMediator.Results;
```
Result [#resultt]
A readonly struct that represents either a successful value or an error:
```csharp
public readonly struct Result
{
public bool IsSuccess { get; }
public bool IsFailure { get; }
public TValue Value { get; } // throws if failure
public Exception Error { get; } // throws if success
}
```
Creating Results [#creating-results]
```csharp
// Success
var success = Result.Success(42);
var success2 = Result.Success(new User("Alice"));
// Failure
var failure = Result.Failure(new InvalidOperationException("oops"));
// From try-catch
var result = Result.Try(() => int.Parse("abc")); // Failure with FormatException
var asyncResult = await Result.TryAsync(() => httpClient.GetStringAsync(url));
```
Implicit Conversions [#implicit-conversions]
```csharp
// Value → Success result
Result result = 42;
// Exception → Failure result
Result