Foundatio.Mediator
1.0.0-preview4
Prefix Reserved
See the version list below for details.
dotnet add package Foundatio.Mediator --version 1.0.0-preview4
NuGet\Install-Package Foundatio.Mediator -Version 1.0.0-preview4
<PackageReference Include="Foundatio.Mediator" Version="1.0.0-preview4"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="Foundatio.Mediator" Version="1.0.0-preview4" />
<PackageReference Include="Foundatio.Mediator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Foundatio.Mediator --version 1.0.0-preview4
#r "nuget: Foundatio.Mediator, 1.0.0-preview4"
#:package Foundatio.Mediator@1.0.0-preview4
#addin nuget:?package=Foundatio.Mediator&version=1.0.0-preview4&prerelease
#tool nuget:?package=Foundatio.Mediator&version=1.0.0-preview4&prerelease
Foundatio.Mediator
Blazingly fast, convention-based C# mediator powered by source generators and interceptors.
β¨ Why Choose Foundatio.Mediator?
- π Near-direct call performance, zero runtime reflection
- β‘ Convention-based handler discovery (no interfaces/base classes)
- π§ Full DI support via Microsoft.Extensions.DependencyInjection
- π§© Plain handler classes or static methodsβjust drop them in
- πͺ Middleware pipeline with Before/After/Finally hooks
- π― Built-in Result and Result<T> types for rich status handling
- π Automatic cascading messages via tuple returns
- π Compile-time diagnostics and validation
- π§ͺ Easy testing since handlers are plain objects and not tied to messaging framework
- π Superior debugging experience with short, simple call stacks
π Quick Start Guide
1. Install the Package
dotnet add package Foundatio.Mediator
2. Register the Mediator
services.AddMediator();
π§© Simple Handler Example
Just add a plain class (instance or static) ending with Handler
or Consumer
. Methods must be named Handle(Async)
or Consume(Async)
. First parameter is required and is always the message.
public record Ping(string Text);
public static class PingHandler
{
public static string Handle(Ping msg) => $"Pong: {msg.Text}";
}
Call it:
var reply = mediator.Invoke<string>(new Ping("Hello"));
π§ Dependency Injection in Handlers
Supports constructor and method injection:
public class EmailHandler
{
private readonly ILogger<EmailHandler> _logger;
public EmailHandler(ILogger<EmailHandler> logger) => _logger = logger;
public Task HandleAsync(SendEmail cmd, IEmailService svc, CancellationToken ct)
{
_logger.LogInformation("Sending to {To}", cmd.To);
return svc.SendAsync(cmd.To, cmd.Subject, cmd.Body, ct);
}
}
π― Using Result<T> in Handlers
Result<T> is our built-in discriminated union for message-oriented workflows, capturing success, validation errors, conflicts, not found states, and moreβwithout relying on exceptions.
public class UserHandler
{
public async Task<Result<User>> HandleAsync(GetUser query) {
var user = await _repo.Find(query.Id);
if (user == null)
return Result.NotFound($"User {query.Id} not found");
// implicitly converted to Result<User>
return user;
}
public async Task<Result<User>> HandleAsync(CreateUser cmd)
{
var user = new User {
Id = Guid.NewGuid(),
Name = cmd.Name,
Email = cmd.Email,
CreatedAt = DateTime.UtcNow
};
await _repo.AddAsync(user);
return user;
}
}
β‘οΈ Invocation API Overview
// Async with response
var user = await mediator.InvokeAsync<User>(new GetUser(id));
// Async without response
await mediator.InvokeAsync(new Ping("Hi"));
// Sync with response (all handlers and middleware must be sync)
var reply = mediator.Invoke<string>(new Ping("Hello"));
πͺ Simple Middleware Example
Just add a class (instance or static) ending with Middleware
. Supports Before(Async)
, After(Async)
and Finally(Async)
lifecycle events. First parameter is required and is always the message. Use object
for all message types or an interface for a subset of messages. HandlerResult
can be returned from the Before
lifecycle method to enable short-circuiting message handling. Other return types from Before
will be available as parameters to After
and Finally
.
public static class ValidationMiddleware
{
public static HandlerResult Before(object msg) {
if (!TryValidate(msg, out var errors))
{
// short-circuit handler results when messages are invalid
return HandlerResult.ShortCircuit(Result.Invalid(errors));
}
return HandlerResult.Continue();
}
}
π Logging Middleware Example
public class LoggingMiddleware(ILogger<LoggingMiddleware> log)
{
// Stopwatch will be available as a parameter in `Finally` method
public Stopwatch Before(object msg) => Stopwatch.StartNew();
// Having a Finally causes handler to be run in a try catch and is guaranteed to run
public void Finally(object msg, Stopwatch sw, Exception? ex)
{
sw.Stop();
if (ex != null)
log.LogInformation($"Error in {msg.GetType().Name}: {ex.Message}");
else
log.LogInformation($"Handled {msg.GetType().Name} in {sw.ElapsedMilliseconds}ms");
}
}
π Tuple Returns & Cascading Messages
Handlers can return tuples; one matches the response, the rest are published:
public async Task<(User user, UserCreated? evt)> HandleAsync(CreateUser cmd)
{
var user = await _repo.Add(cmd);
return (user, new UserCreated(user.Id));
}
// Usage
var user = await mediator.InvokeAsync<User>(new CreateUser(...));
// UserCreated is auto-published and any handlers are invoked inline before this method returns
Middleware Short-Circuit Behavior
When middleware short-circuits handler execution by returning a HandlerResult
, the short-circuited value becomes the first tuple element, while all other tuple elements are set to their default values (null for reference types, default values for value types):
public class ValidationMiddleware
{
public HandlerResult Before(CreateUser cmd) {
if (!IsValid(cmd))
{
var errorResult = Result.Invalid("Invalid user data");
return HandlerResult.ShortCircuit(errorResult);
}
return HandlerResult.Continue();
}
}
// If validation fails, the tuple result will be:
// (Result.Invalid("Invalid user data"), null)
// where the User is the error result and UserCreated event is null
π Streaming Handler Support
Handlers can return IAsyncEnumerable<T>
for streaming scenarios:
public record CounterStreamRequest { }
public class StreamingHandler
{
public async IAsyncEnumerable<int> HandleAsync(CounterStreamRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
if (cancellationToken.IsCancellationRequested)
yield break;
await Task.Delay(1000, cancellationToken);
yield return i;
}
}
}
// Usage
await foreach (var item in mediator.Invoke<IAsyncEnumerable<int>>(new CounterStreamRequest(), ct))
{
Console.WriteLine($"Counter: {item}");
}
π¦ Publish API & Behavior
Sends a message to zero or more handlers; all are invoked inline and in parallel.
If any handler fails, PublishAsync
throws (aggregates) exceptions.
await mediator.PublishAsync(new OrderShipped(orderId));
π Performance Benchmarks
Foundatio.Mediator delivers exceptional performance, getting remarkably close to direct method calls while providing full mediator pattern benefits:
Commands
Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct |
---|---|---|---|---|---|---|
Direct_Command | 8.33 ns | 0.17 ns | 0.24 ns | - | 0 B | baseline |
Foundatio_Command | 17.93 ns | 0.36 ns | 0.34 ns | - | 0 B | 2.15x |
MediatR_Command | 54.81 ns | 1.12 ns | 1.77 ns | 0.0038 | 192 B | 6.58x |
MassTransit_Command | 1,585.85 ns | 19.82 ns | 17.57 ns | 0.0839 | 4232 B | 190.4x |
Queries (Request/Response)
Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct |
---|---|---|---|---|---|---|
Direct_Query | 32.12 ns | 0.50 ns | 0.47 ns | 0.0038 | 192 B | baseline |
Foundatio_Query | 46.36 ns | 0.94 ns | 0.84 ns | 0.0052 | 264 B | 1.44x |
MediatR_Query | 81.40 ns | 1.32 ns | 1.23 ns | 0.0076 | 384 B | 2.53x |
MassTransit_Query | 6,354.47 ns | 125.37 ns | 195.19 ns | 0.2518 | 12784 B | 197.8x |
Events (Publish/Subscribe)
Method | Mean | Error | StdDev | Gen0 | Allocated | vs Direct |
---|---|---|---|---|---|---|
Direct_Event | 8.12 ns | 0.18 ns | 0.36 ns | - | 0 B | baseline |
Foundatio_Publish | 121.57 ns | 0.80 ns | 0.71 ns | 0.0134 | 672 B | 15.0x |
MediatR_Publish | 59.29 ns | 1.13 ns | 1.59 ns | 0.0057 | 288 B | 7.30x |
MassTransit_Publish | 1,697.53 ns | 13.97 ns | 13.06 ns | 0.0877 | 4448 B | 209.0x |
Dependency Injection Overhead
Method | Mean | Error | StdDev | Gen0 | Allocated | vs No DI |
---|---|---|---|---|---|---|
Direct_QueryWithDependencies | 39.24 ns | 0.81 ns | 1.28 ns | 0.0052 | 264 B | baseline |
Foundatio_QueryWithDependencies | 53.30 ns | 1.05 ns | 1.37 ns | 0.0067 | 336 B | 1.36x |
MediatR_QueryWithDependencies | 79.97 ns | 0.54 ns | 0.51 ns | 0.0091 | 456 B | 2.04x |
MassTransit_QueryWithDependencies | 5,397.69 ns | 61.05 ns | 50.98 ns | 0.2518 | 12857 B | 137.6x |
π― Key Performance Insights
- π Near-Optimal Performance: Only slight overhead vs direct method calls
- β‘ Foundatio vs MediatR: 3.06x faster for commands, 1.76x faster for queries
- π― Foundatio vs MassTransit: 88x faster for commands, 137x faster for queries
- πΎ Zero Allocation Commands: Fire-and-forget operations have no GC pressure
- π₯ Minimal DI Overhead: Only 36% performance cost for dependency injection
- π‘ Efficient Publishing: Event publishing scales well with multiple handlers
Benchmarks run on .NET 9.0 with BenchmarkDotNet. Results show Foundatio.Mediator achieves its design goal of getting as close as possible to direct method call performance.
π― Handler Conventions
Class Names
Handler classes must end with:
Handler
Consumer
Method Names
Valid handler method names:
Handle
/HandleAsync
Handles
/HandlesAsync
Consume
/ConsumeAsync
Consumes
/ConsumesAsync
Method Signatures
- First parameter: the message object
- Remaining parameters: injected via DI (including
CancellationToken
) - Return type: any type (including
void
,Task
,Task<T>
)
Dependency Injection Support:
- Constructor injection: Handler classes support full constructor DI
- Method injection: Handler methods can declare any dependencies as parameters
- Known parameters:
CancellationToken
is automatically provided by the mediator - Service resolution: All other parameters are resolved from the DI container
- Handler lifetime: Handlers are singleton instances by default. Register handlers in DI for custom lifetime behavior
Ignoring Handlers
- Annotate handler classes or methods with
[FoundatioIgnore]
to exclude them from discovery
πͺ Middleware Conventions
- Classes should end with
Middleware
- Valid method names:
Before(...)
/BeforeAsync(...)
After(...)
/AfterAsync(...)
Finally(...)
/FinallyAsync(...)
- First parameter must be the message (can be
object
, an interface, or a concrete type) - Lifecycle methods are optionalβyou can implement any subset (
Before
,After
,Finally
) Before
can return:- a
HandlerResult
to short-circuit execution - a
Result?
and if the handler also returns aResult
orResult<T>
returning non-null will short-circuit execution - a single state value
- a tuple of state values
- a
- Values (single or tuple elements) returned from
Before
are matched by type and injected intoAfter
/Finally
parameters After
runs only on successful handler completionFinally
always runs, regardless of success or failure- Methods may declare additional parameters:
CancellationToken
, DI-resolved services
Middleware Order
Use [FoundatioOrder(int)]
to control execution order. Lower values execute first in Before
, last in After
/Finally
(reverse order for proper nesting). Without explicit ordering, middleware follows: message-specific β interface-based β object-based.
Ignoring Middleware
- Annotate middleware classes or methods with
[FoundatioIgnore]
to exclude them from discovery
π§ API Reference
IMediator Interface
public interface IMediator
{
// Async operations
Task InvokeAsync(object message, CancellationToken cancellationToken = default);
Task<TResponse> InvokeAsync<TResponse>(object message, CancellationToken cancellationToken = default);
// Sync operations
void Invoke(object message, CancellationToken cancellationToken = default);
TResponse Invoke<TResponse>(object message, CancellationToken cancellationToken = default);
// Publishing (multiple handlers)
Task PublishAsync(object message, CancellationToken cancellationToken = default);
}
βοΈ How It Works
The source generator:
- Discovers handlers at compile time by scanning for classes ending with
Handler
orConsumer
- Discovers handler methods looks for methods with names like
Handle
,HandleAsync
,Consume
,ConsumeAsync
- Parameters first parameter is the message, remaining parameters are injected via DI
- Generates C# interceptors for blazing fast same-assembly dispatch using direct method calls
- Middleware can run
Before
,After
, andFinally
around handler execution and can be sync or async - Handler lifetime handlers are singleton instances by default (not registered in DI). Register handlers in DI for custom lifetime behavior
π§ C# Interceptors - The Secret Sauce
Foundatio.Mediator uses a dual dispatch strategy for maximum performance and flexibility:
π Same-Assembly: C# Interceptors (Blazing Fast)
// You write this:
await mediator.InvokeAsync(new PingCommand("123"));
// The source generator intercepts and transforms it to essentially this:
await PingHandler_Generated.HandleAsync(new PingCommand("123"), serviceProvider, cancellationToken);
π Cross-Assembly & Publish: DI Registration (Flexible)
// For handlers in other assemblies or publish scenarios:
// Falls back to keyed DI registration lookup
var handlers = serviceProvider.GetKeyedServices<HandlerRegistration>("MyApp.PingCommand");
foreach (var handler in handlers)
await handler.HandleAsync(mediator, message, cancellationToken, responseType);
How the Dual Strategy Works:
- Interceptors First - Same-assembly calls use interceptors for maximum performance
- DI Fallback - Cross-assembly handlers and publish operations use DI registration
- Zero Runtime Overhead - Interceptors bypass all runtime lookup completely
Benefits:
- π Maximum Performance - Interceptors are as fast as calling handler methods directly
- π Cross-Assembly Support - DI registration enables handlers across multiple projects
- π’ Publish Support - Multiple handlers per message via DI enumeration
- πΎ Zero Allocations - Interceptors have no boxing, delegates, or intermediate objects
- π Full IntelliSense - All the tooling benefits of regular method calls
- π‘οΈ Type Safety - Compile-time verification of message types and return values
The generated code is as close to direct method calls as possible, with minimal overhead.
π Compile-Time Safety
The source generator provides compile-time errors for:
- Missing handlers for a message type
- Multiple handlers for the same message type
- Invalid handler method signatures
- Using sync methods when only async handlers exist
- Middleware configuration issues
π Remaining Work
- Simplify tests to use Roslyn source generator testing utilities and have it generate code in memory and do asserts there instead of having all integration tests
π License
MIT License
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Foundatio.Mediator.Abstractions (>= 1.0.0-preview4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
1.0.0-preview9 | 246 | 9/16/2025 |
1.0.0-preview8 | 131 | 8/18/2025 |
1.0.0-preview7 | 132 | 8/14/2025 |
1.0.0-preview6 | 174 | 8/8/2025 |
1.0.0-preview5 | 152 | 8/8/2025 |
1.0.0-preview4 | 195 | 8/6/2025 |
1.0.0-preview3 | 191 | 8/5/2025 |
1.0.0-preview2 | 463 | 7/24/2025 |
1.0.0-preview12 | 45 | 9/21/2025 |
1.0.0-preview11 | 250 | 9/17/2025 |
1.0.0-preview10 | 253 | 9/16/2025 |
1.0.0-preview | 505 | 7/22/2025 |