Skip to content

Repository files navigation

Switchboard

NuGet CI License

A lightweight, MediatR-compatible mediator for .NET.

📖 Overview, migration guide and FAQ: softwarefirst.gr/switchboard

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 dependencyMicrosoft.Extensions.DependencyInjection.Abstractions, floored at the lowest patch of each major so it never drags your other Microsoft.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

  1. Replace the MediatR package reference with SoftwareFirst.Switchboard.
  2. Replace using MediatR; with using Switchboard;.
  3. Replace services.AddMediatR(...) with services.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 CancellationToken passed to Send flows to every behavior and the handler, even when a behavior calls next() 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

Apache 2.0

About

A lightweight, drop-in MediatR alternative for .NET 8/9/10 — requests, notifications and pipeline behaviors over Microsoft.Extensions.DependencyInjection. One dependency, a few hundred lines, Apache-2.0.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages