Foundatio.Mediator 1.0.0-preview4

Prefix Reserved
This is a prerelease version of Foundatio.Mediator.
There is a newer prerelease version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Foundatio.Mediator" Version="1.0.0-preview4">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Foundatio.Mediator" Version="1.0.0-preview4" />
                    
Directory.Packages.props
<PackageReference Include="Foundatio.Mediator">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Foundatio.Mediator --version 1.0.0-preview4
                    
#r "nuget: Foundatio.Mediator, 1.0.0-preview4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Foundatio.Mediator@1.0.0-preview4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Foundatio.Mediator&version=1.0.0-preview4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Foundatio.Mediator&version=1.0.0-preview4&prerelease
                    
Install as a Cake Tool

Foundatio.Mediator

NuGet

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 a Result or Result<T> returning non-null will short-circuit execution
    • a single state value
    • a tuple of state values
  • Values (single or tuple elements) returned from Before are matched by type and injected into After/Finally parameters
  • After runs only on successful handler completion
  • Finally 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:

  1. Discovers handlers at compile time by scanning for classes ending with Handler or Consumer
  2. Discovers handler methods looks for methods with names like Handle, HandleAsync, Consume, ConsumeAsync
  3. Parameters first parameter is the message, remaining parameters are injected via DI
  4. Generates C# interceptors for blazing fast same-assembly dispatch using direct method calls
  5. Middleware can run Before, After, and Finally around handler execution and can be sync or async
  6. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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