SoftwareFirst.Switchboard
1.1.0
dotnet add package SoftwareFirst.Switchboard --version 1.1.0
NuGet\Install-Package SoftwareFirst.Switchboard -Version 1.1.0
<PackageReference Include="SoftwareFirst.Switchboard" Version="1.1.0" />
<PackageVersion Include="SoftwareFirst.Switchboard" Version="1.1.0" />
<PackageReference Include="SoftwareFirst.Switchboard" />
paket add SoftwareFirst.Switchboard --version 1.1.0
#r "nuget: SoftwareFirst.Switchboard, 1.1.0"
#:package SoftwareFirst.Switchboard@1.1.0
#addin nuget:?package=SoftwareFirst.Switchboard&version=1.1.0
#tool nuget:?package=SoftwareFirst.Switchboard&version=1.1.0
Switchboard
A lightweight, MediatR-compatible mediator for .NET.
Switchboard implements the request/response, notification, and pipeline-behavior surface of MediatR on top of Microsoft.Extensions.DependencyInjection, in a few hundred lines of code with a single dependency (Microsoft.Extensions.DependencyInjection.Abstractions). It was extracted from a production system that moved off MediatR when it became commercially licensed: swap your using directives, change one registration call, and your handlers, behaviors, and call sites compile unchanged.
Install
dotnet add package SoftwareFirst.Switchboard
Targets net8.0, net9.0 and net10.0, so you can move off MediatR without moving frameworks first. The package ID is prefixed, but the assembly and namespace are both plain Switchboard — you write using Switchboard;.
Why this one
Several MediatR alternatives exist now, and most compete on speed or feature count. Switchboard competes on being small:
- A few hundred lines, across about a dozen files you can read end to end in one sitting.
- One dependency —
Microsoft.Extensions.DependencyInjection.Abstractions, floored at the lowest patch of each major so it never drags your otherMicrosoft.Extensions.*packages forward. - No source generators, analyzers, or build-time magic. Plain reflection over the DI container, the way MediatR does it.
- A deliberately identical API surface — the migration is a find-and-replace, not a rewrite.
- Apache 2.0, extracted from a production system that made this exact switch.
If you need streaming, parallel publish strategies, or maximum throughput, a source-generated alternative is the better fit — the migration table below says so explicitly.
Quick start
Define a request and its handler:
using Switchboard;
public sealed record GetOrder(int Id) : IRequest<OrderDto>;
public sealed class GetOrderHandler : IRequestHandler<GetOrder, OrderDto>
{
public Task<OrderDto> Handle(GetOrder request, CancellationToken cancellationToken)
=> /* ... */;
}
Register the mediator and send:
services.AddSwitchboard(cfg => cfg
.RegisterServicesFromAssemblyContaining<GetOrderHandler>());
public sealed class OrderController(ISender sender) : ControllerBase
{
[HttpGet("{id}")]
public Task<OrderDto> Get(int id, CancellationToken ct) => sender.Send(new GetOrder(id), ct);
}
Void requests implement IRequest (no type argument) and are handled by IRequestHandler<TRequest>.
Pipeline behaviors
Behaviors wrap every handler, outermost first in the order they are added:
public sealed class LoggingBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(
TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
// before
var response = await next(cancellationToken);
// after
return response;
}
}
services.AddSwitchboard(cfg => cfg
.RegisterServicesFromAssemblyContaining<GetOrderHandler>()
.AddOpenBehavior(typeof(LoggingBehaviour<,>)) // runs outermost
.AddOpenBehavior(typeof(ValidationBehaviour<,>))); // runs inside logging
A behavior that applies to one specific request/response pair goes in with AddBehavior:
services.AddSwitchboard(cfg => cfg
.RegisterServicesFromAssemblyContaining<GetOrderHandler>()
.AddOpenBehavior(typeof(LoggingBehaviour<,>)) // outermost
.AddBehavior<AuditGetOrder>() // IPipelineBehavior<GetOrder, OrderDto>
.AddOpenBehavior(typeof(ValidationBehaviour<,>))); // innermost
Open and closed behaviors share a single ordering, so the first one added is outermost regardless of which kind it is. Registering directly against the container still works too:
services.AddTransient<IPipelineBehavior<GetOrder, OrderDto>, MyBehavior>().
Void requests run through the same pipeline with TResponse == Unit, so open-generic behaviors apply to them unchanged.
Notifications
public sealed record OrderPlaced(int OrderId) : INotification;
public sealed class SendReceipt : INotificationHandler<OrderPlaced> { /* ... */ }
public sealed class UpdateStats : INotificationHandler<OrderPlaced> { /* ... */ }
await publisher.Publish(new OrderPlaced(42), cancellationToken);
Handlers run sequentially, in registration order — never in parallel — so they can safely share scoped state such as an EF Core DbContext.
Migrating from MediatR
- Replace the
MediatRpackage reference withSoftwareFirst.Switchboard. - Replace
using MediatR;withusing Switchboard;. - Replace
services.AddMediatR(...)withservices.AddSwitchboard(...)— the configuration methods (RegisterServicesFromAssemblyContaining,RegisterServicesFromAssembly,AddOpenBehavior) keep their names.
| MediatR feature | Switchboard |
|---|---|
IRequest, IRequest<T>, IRequestHandler<,>, IRequestHandler<> |
✅ identical |
INotification, INotificationHandler<> |
✅ identical |
IPipelineBehavior<,> (first registered runs outermost) |
✅ identical |
ISender, IPublisher, IMediator, Unit |
✅ identical |
Untyped Send(object) / Publish(object) |
✅ identical |
| Assembly scanning for handlers | ✅ identical |
Streaming (IStreamRequest<>) |
❌ not implemented |
| Request pre-/post-processors | ❌ use a pipeline behavior |
Exception handlers/actions (IRequestExceptionHandler) |
❌ use a pipeline behavior |
| Custom publish strategies (parallel, etc.) | ❌ sequential only |
Semantics worth knowing
- Cancellation is never lost. The
CancellationTokenpassed toSendflows to every behavior and the handler, even when a behavior callsnext()without arguments. - Handlers and behaviors are transient; they are resolved from the scope the mediator was resolved from, so scoped dependencies work as expected.
- Publishing to zero handlers is a no-op, mirroring MediatR.
- Handler-type wrappers are cached statically per request type; the cache is stateless and thread-safe.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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 is compatible. 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. |
-
net10.0
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Adds net8.0 and net9.0 targets alongside net10.0, so the package can be adopted without a framework upgrade. No API changes.