SwitchMediator
A Source-Generated Mediator for C#, Created Specifically to Address Pet Peeves with MediatR - but MUCH Faster and with MUCH Less Allocations!
Install / Use
/learn @zachsaw/SwitchMediatorREADME
SwitchMediator
SwitchMediator: A High-Performance, Source-Generated Mediator for .NET
SwitchMediator is a zero-allocation, AOT-friendly implementation of the mediator pattern, designed to be API-compatible with popular libraries like MediatR.
By leveraging C# Source Generators, SwitchMediator moves the heavy lifting from runtime to compile time. Instead of scanning assemblies and using Reflection for every dispatch, it generates a static, type-safe lookup (using FrozenDictionary on .NET 8+) that routes messages to handlers instantly.
The result is a mediator that offers:
- Zero Runtime Reflection: No scanning cost at startup.
- AOT & Trimming Compatibility: Native support for modern .NET deployment models.
- Compile-Time Safety: Missing handlers are caught during the build, not at runtime.
- Step-Through Debugging: You can step directly into the generated dispatch code to see exactly how your pipeline works.
Table of Contents
- What's New in V3.2
- What's New in V3.1
- What's New in V3
- What's New in V2
- Why SwitchMediator?
- 🌟 Feature Spotlight: True Polymorphic Dispatch
- Key Advantages
- Features
- Installation
- Usage Example
- License
What's New in V3.2
Self-Referential Pipeline Constraints
V3.2 fixes behavior applicability checks for self-referential generic constraints in request/value-request pipelines. This primarily affects advanced patterns where a request or behavior constrains TResponse using the same type parameter recursively, for example where TResponse : struct, IErrorResultFactory<TResponse>.
Both Task-based and ValueTask-based pipelines are supported here. In V3.2, the generated mediator now applies matching pipeline behaviors correctly for self-referential constraint patterns such as error-result factories, OneOf-style responses, and similar static-abstract factory shapes.
What's New in V3.1
Hybrid ValueTask Support
V3.1 introduces IValueMediator, IValueSender, and IValuePublisher — a parallel set of interfaces that dispatch via ValueTask instead of Task.
The generated mediator class now implements both IMediator and IValueMediator simultaneously. You can inject whichever interface suits your performance needs.
New interfaces added:
| Interface | Purpose |
| :--- | :--- |
| IValueSender | ValueTask<TResponse> Send(IRequest<TResponse>, CancellationToken) |
| IValuePublisher | ValueTask Publish(INotification, CancellationToken) |
| IValueMediator | Combines IValueSender + IValuePublisher |
| IValueRequestHandler<TRequest, TResponse> | ValueTask-returning handler |
| IValueNotificationHandler<TNotification> | ValueTask-returning notification handler |
| IValuePipelineBehavior<TRequest, TResponse> | ValueTask-returning pipeline behavior |
| IValueNotificationPipelineBehavior<TNotification> | ValueTask-returning notification pipeline behavior |
Allocation characteristics:
| Path | Allocation |
| :--- | :--- |
| IValuePublisher.Publish + IValueNotificationHandler | Zero allocation in the dispatch layer |
| IPublisher.Publish + INotificationHandler (existing) | Already zero allocation (unchanged) |
| IValueSender.Send + IValueRequestHandler | Zero allocation — fully alloc-free in the mediator infrastructure |
| ISender.Send + IRequestHandler (existing) | ~96 B per call (unchanged) |
Note:
IValueSender.Sendpaired withIValueRequestHandler(and no pipeline behaviors) is fully alloc-free.IValuePublisher.Publishuses the same zero-cost dictionary routing, but the generated notification fan-out uses async iteration, which may allocate when your handlers are genuinely asynchronous (as per normal usage of .netValueTask).
New Compile-Time Analyzer: SMD002
A new PipelineConsistencyAnalyzer (diagnostic ID SMD002) reports a build error if you accidentally mix IPipelineBehavior (Task) behaviors with IValueRequestHandler (ValueTask) handlers, or vice versa.
Why does this happen?
SwitchMediator automatically applies behaviors to handlers based on generic constraints. A behavior like ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull applies to all requests (since all requests satisfy notnull). If you also have a ValueTask handler for the same request type, the analyzer detects the mismatch and reports SMD002.
// ❌ SMD002 error: Generic IPipelineBehavior applies to ValueTask handler
public class FastStatusCheckHandler : IValueRequestHandler<FastStatusCheckRequest, bool> { ... }
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull // This applies to ALL requests — including FastStatusCheckRequest!
{ ... }
// ✅ Fix 1: Constrain the behavior so it only applies to specific requests
public interface IValidatable { }
public class FastStatusCheckRequest : IRequest<bool> { ... } // No IValidatable — behavior doesn't apply
public class UserUpdateRequest : IRequest<bool>, IValidatable { ... } // Has IValidatable — behavior applies
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull, IValidatable // Now only applies to requests implementing IValidatable
{ ... }
// ✅ Fix 2: Create a matching ValueTask version for behaviors that should apply to both pipelines
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull { ... } // For Task handlers
public class ValidationValueBehavior<TRequest, TResponse> : IValuePipelineBehavior<TRequest, TResponse>
where TRequest : notnull { ... } // For ValueTask handlers
How the analyzer works: The analyzer checks each behavior's generic constraints against handler types. A behavior only applies if the request type satisfies all of its constraints (e.g., where TRequest : notnull, IValidatable). This allows you to selectively apply behaviors to specific request types while avoiding cross-pipeline contamination.
Self-referential generic constraints are supported too. This matters for ValueTask-based pipelines that model OneOf/Result-style responses using constraints like where TResponse : struct, IErrorResultFactory<TResponse>. SwitchMediator now correctly recognizes those behaviors as applicable when the request constraint flows the same response type through the pipeline.
public interface IErrorResultFactory<TSelf>
where TSelf : struct, IErrorResultFactory<TSelf>
{
static abstract TSelf CreateFromError(string error);
}
public interface IOneOfRequest<TResponse> : IRequest<TResponse>
where TResponse : struct, IErrorResultFactory<TResponse>;
public sealed class DeleteMenuItemCommand : IOneOfRequest<MenuOperationResult> { }
public readonly struct MenuOperationResult : IErrorResultFactory<MenuOperationResult>
{
public static MenuOperationResult CreateFromError(string error) => new(isError: true);
}
public sealed class RecoverableValueBehavior<TRequest, TResponse> : IValuePipelineBehavior<TRequest, TResponse>
where TRequest : class, IOneOfRequest<TResponse>
where TResponse : struct, IErrorResultFactory<TResponse>
{
public async ValueTask<TResponse> Handle(
TRequest request,
ValueRequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
try
{
return await next(cancellationToken);
}
catch (Exception ex)
{
return TResponse.CreateFromError(ex.Message);
}
}
}
This is included in sample/Sample.ConsoleApp so you can see both the successful path and the fallback error-result path in a working example.
DI Registration
AddMediator<T>() automatically registers IValueMediator, IValueSender, and IValuePublisher alongside the existing IMediator, ISender, and IPublisher. No extra configuration needed.
// All six interfaces are available after a single AddMediator call
var sender = sp.GetRequiredService<ISender>(); // Task-based
var valueSender = sp.GetRequiredService<IValueSender>(); // ValueTask-based (zero extra setup)
What's New in V3
⚠️ Breaking Change: User-Defined Partial Class
In previous versions, the library automatically generated a class named SwitchMediator. In V3, you must define the mediator class yourself as a partial class and mark it with the [SwitchMediator] attribute.
Why?
- Namespace Control: You can now place the mediator in any namespace you choose.
- Visibility Control: You decide if your mediator is
publicorinternal.
⚠️ Breaking Change: KnownTypes Location
The KnownTypes property is no longer on a static SwitchMediator class. It is now generated as a static property on your custom partial class.
What's New in V2
1. Notification Pipeline Behaviors
V2 introduced support for pipeline behaviors on Notifications.
SwitchMediator wraps each handler execution independently in its own pipeline scope. This enables powerful patterns like *
