Mordecai 2026.9.25.1
dotnet add package Mordecai --version 2026.9.25.1
NuGet\Install-Package Mordecai -Version 2026.9.25.1
<PackageReference Include="Mordecai" Version="2026.9.25.1" />
<PackageVersion Include="Mordecai" Version="2026.9.25.1" />
<PackageReference Include="Mordecai" />
paket add Mordecai --version 2026.9.25.1
#r "nuget: Mordecai, 2026.9.25.1"
#:package Mordecai@2026.9.25.1
#addin nuget:?package=Mordecai&version=2026.9.25.1
#tool nuget:?package=Mordecai&version=2026.9.25.1
Mordecai
A high-performance, in-process mediator for .NET. Requests go to exactly one handler, notifications broadcast to many, and a middleware pipeline wraps around both.
The governing design goal is a fast send path with zero steady-state allocation: discovery happens once at startup, and everything after that is a cached lookup and a virtual call.
Status: the engine is implemented, tested, and benchmarked, and published on NuGet. Targets
net10.0; runs on .NET 10 and later.
Quick start
Install:
dotnet add package Mordecai
Define a request:
using Mordecai;
public sealed record Order(int Id, string Status);
public sealed record GetOrder(int OrderId) : IRequest<Order>;
Write its handler — exactly one per request:
public sealed class GetOrderHandler : IRequestHandler<GetOrder, Order>
{
public Task<Order> Handle(GetOrder request, CancellationToken cancellationToken)
=> Task.FromResult(new Order(request.OrderId, "Shipped"));
}
Register, naming the assemblies that contain your handlers:
builder.Services.AddMordecai(cfg =>
cfg.RegisterServicesFromAssemblyContaining<Program>());
Send:
public sealed class OrderLookup(ISender sender)
{
public Task<Order> FindAsync(int orderId, CancellationToken ct)
=> sender.Send(new GetOrder(orderId), ct);
}
That is the whole loop. The usage guide covers the rest.
Performance
From docs/guides/performance.md, which has the machine, the runtime
version, and the byte-by-byte accounting:
| Mean | Allocated | |
|---|---|---|
| Direct handler call (baseline) | 0.21 ns | 0 B |
Send, no behaviors |
23.66 ns | 0 B |
Send through three behaviors |
83.63 ns | 408 B |
The zero is the point, and it is enforced rather than hoped for: new allocation on the send or publish path needs a benchmark showing it is free, or it does not land.
The ~24 ns is a floor, not a shortfall. ISender.Send<TResponse>(IRequest<TResponse>) gives you the
response type statically but not the concrete request type — that is erased at the entry point — so
every send has to recover it at runtime and dispatch through a wrapper that closes over it. One type
lookup, one virtual call. The alternative signature, Send<TRequest, TResponse>, is rejected
because C# will not partially infer type arguments and every call site would have to spell out both.
What's in the box
- Requests —
IRequest<TResponse>, orIRequestfor the void case, which returns a zero-sizeUnitso nothing is allocated for a response nobody reads. - Streaming —
IStreamRequest<T>handlers returningIAsyncEnumerable<T>, with cancellation wired correctly through[EnumeratorCancellation]. - Notifications — many handlers per event, delivered by a swappable
INotificationPublisher.SequentialPublisheris the default;ParallelPublisherstarts everything at once and aggregates the failures. - Pipeline behaviors —
IPipelineBehavior<,>andIStreamPipelineBehavior<,>, nesting in registration order with the first registered outermost. Open-generic and closed registrations share one ordered chain. - Pre- and post-processors — narrower hooks than a behavior, run inside your own pipeline.
- Registration — assembly scanning with deduplication by assembly identity, and duplicate handlers caught at startup rather than at the first send.
What it deliberately does not do
- Native AOT. Handler discovery is assembly scanning, which is reflection, and the docs say so
plainly instead of implying otherwise. The reflective entry points carry
[RequiresUnreferencedCode]and[RequiresDynamicCode], so a trimmed or AOT build warns at your own call site naming the exact method. See usage § 14. - Open-generic handler types. Scanning rejects them with a
NotSupportedExceptionnaming the type. Open-generic behaviors are fully supported and are the intended way to write something that applies across many request types. - Behavior discovery by scanning. Order matters for behaviors and reflection's type order is not stable, so they are registered explicitly. A scanned pipeline would order itself differently between runs.
- Exception-handling behaviors. Not declared yet.
Originality
All code here is original. Mordecai has no dependency on, and takes no code from, any other
mediator library. It uses standard mediator-pattern vocabulary — IRequest, IRequestHandler,
INotification, IPipelineBehavior, ISender, IPublisher, IMediator, Unit — because that is
the common language of the pattern and makes the API legible on sight. Names are the only thing
shared with anything else; every implementation decision is made here on its own merits.
Repository layout
| Project | Role |
|---|---|
src/Mordecai |
Public abstractions and runtime. The shipped package. |
src/Mordecai.SourceGenerator |
Roslyn generator, dormant — the home for future compile-time dispatch. Emits nothing today. |
tests/Mordecai.Tests |
xunit v3. |
tests/Mordecai.Tests.Scanning |
A plain class library, present so the scanner has a well-formed assembly to walk. |
benchmarks/Mordecai.Benchmarks |
BenchmarkDotNet. |
The solution file is Mordecai.slnx, the .NET 10 SDK's XML format — not .sln.
Building
dotnet build Mordecai.slnx -c Release
dotnet test Mordecai.slnx -c Release
dotnet format Mordecai.slnx --verify-no-changes
# Benchmarks are Release-only; BenchmarkDotNet refuses a Debug build.
dotnet run -c Release --project benchmarks/Mordecai.Benchmarks -- --filter "*"
TreatWarningsAsErrors is on repo-wide, and the shipping library opts into trim and AOT analysis,
so those warnings are build errors too. Fix the finding rather than suppressing it.
Documentation
- Usage — installing, registering, and wiring each piece: DI, ASP.NET Core, background services, testing, AOT, troubleshooting.
- Abstractions — every public type, in detail.
- Behaviors — a catalogue of middleware to build with the pipeline: validation, caching, transactions, retry, timeouts, tracing, and where each belongs in the chain.
- Performance — benchmark results and where each byte goes.
- CLAUDE.md — architecture, hard rules, and implementation status.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
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 |
|---|---|---|
| 2026.9.25.1 | 120 | 9/25/2026 |