
Why explicit registration, deterministic behavior, and conscious trade-offs sometimes matter more than flexibility
Most mediator libraries in .NET optimize for flexibility and convenience. Handlers are discovered automatically, pipelines are composed implicitly, and the system adapts as your application grows.
This works well for many teams — until it doesn’t. At a certain scale, or under certain constraints, implicit wiring starts to blur system boundaries, introduce startup surprises, and make reasoning about behavior harder than it needs to be.
This article is about exploring a different set of trade-offs: what happens if a mediator is explicit by default, deliberately limited in scope, and designed to behave predictably rather than magically.
I ran into these questions repeatedly in real systems — especially those with stricter startup and deployment constraints. Each time, I found myself wanting a mediator that made fewer assumptions and fewer decisions on my behalf.
Liaison.Mediator grew out of that frustration. It’s not meant to replace MediatR or similar libraries, but to serve as a deliberately narrower experiment in explicit wiring and predictable behavior.
Why explicit registration
Explicit registration is often framed as a loss of convenience. Compared to assembly scanning, it requires more upfront work and makes wiring decisions visible in code rather than inferred at runtime.
For many applications, that extra friction is unnecessary and offers little practical benefit. Automatic discovery works, scales well, and keeps configuration minimal. There is nothing inherently wrong with that approach.
The trade-off appears when systems grow, constraints tighten, or expectations around predictability increase. At that point, implicit wiring can make it difficult to answer basic questions with confidence: which handlers are actually registered, in what order they execute, and what behavior changes when a new assembly is added.
By making registration explicit, those questions move from runtime behavior to compile-time intent. Adding a handler becomes a deliberate change, not a side effect. The wiring is visible, reviewable, and predictable — even if that means writing a few more lines of code.
Deterministic behavior as a design goal
Explicit registration makes wiring visible, but visibility alone does not guarantee predictable behavior.
In many mediator implementations, runtime behavior emerges from a mix of conventions: discovery rules, pipeline ordering, implicit lifetimes, and framework defaults. Each piece may be reasonable in isolation, but their interaction is often hard to reason about by looking at the code alone.
Designing for determinism shifts the goal. The focus is not on maximizing flexibility, but on ensuring that the same inputs always produce the same observable behavior — regardless of build configuration, runtime environment, or unrelated assemblies.
In practice, this means preferring explicit ordering, well-defined execution rules, and minimal reliance on ambient state. When behavior changes, it should be possible to point to a specific line of code and say: this is why it changed.
That predictability comes at a cost. It usually requires giving up some late-bound extensibility in exchange for systems that are easier to understand, review, and trust under constraints.
Conscious limitations instead of growing abstractions
Many infrastructure libraries grow by accumulating features. Each addition solves a real problem, but over time the abstraction itself becomes harder to understand than the problems it was meant to address.
Choosing conscious limitations is an alternative path. Instead of asking what else a mediator could support, the question becomes what it should explicitly avoid supporting in order to remain predictable and easy to reason about.
In practice, this means resisting late-bound extensibility, global behaviors, and deeply customizable pipelines. These features are powerful, but they also tend to obscure execution flow and introduce coupling that only becomes visible under stress.
By staying deliberately narrow, the abstraction remains stable. Its behavior can be described succinctly, reviewed confidently, and relied upon without requiring deep knowledge of hidden extension points.
This approach is not universally better. It trades breadth for clarity — and only makes sense in systems where clarity and predictability are valued more than maximum flexibility.
This is not a MediatR replacement — and that’s intentional
Liaison.Mediator is not intended to replace MediatR or similar libraries. Those tools solve a broader set of problems and offer levels of extensibility that many applications genuinely need.
The design choices described here deliberately narrow that scope. Features like assembly scanning, global behaviors, or highly customizable pipelines are not missing by accident — they are excluded to preserve explicit wiring and predictable behavior.
If your system relies on these capabilities, MediatR is likely a better fit. Liaison.Mediator does not aim to cover every mediator use case, only a specific subset where determinism, clarity, and limited surface area are more valuable than flexibility.
Seen this way, the library is less an alternative and more a concrete exploration of a different set of priorities — one that may or may not align with your constraints.
A small note on migration and compatibility
The ideas described here are not tied to a completely new programming model. If you’re already using MediatR, the core concepts — requests, handlers, notifications — are likely familiar.
The primary difference lies in where decisions are made. Instead of relying on implicit discovery and convention-driven wiring, handlers are registered explicitly. As a result, migrating existing request and handler code is usually straightforward, while configuration becomes a deliberate and visible step.
This does not make migration free or automatic. Explicit registration requires touching composition root code and making choices that were previously inferred at runtime. However, those changes tend to be mechanical rather than conceptual.
Compatibility, in this sense, is less about API similarity and more about mental model continuity. The goal is not to provide a drop-in replacement, but to make experimenting with a more explicit approach possible without rewriting large parts of an existing system.
// Implicit, assembly-based registration
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Ping).Assembly));
// Explicit registration
var mediator = new MediatorBuilder()
.RegisterRequestHandler<Ping, Pong>(new PingHandler())
.Build();
Performance, benchmarks, and why they’re not the point
Performance inevitably comes up in discussions about mediator libraries. It is also an area where numbers are easy to misinterpret when taken out of context.
Benchmarks for Liaison.Mediator are included in the repository and are intended to validate a narrow claim: that choosing explicit wiring and deterministic behavior does not introduce unreasonable overhead. They are not meant to demonstrate superiority, nor to serve as a proxy for real-world application performance.
In practice, mediator performance is rarely a primary bottleneck. The more meaningful costs tend to come from I/O, serialization, and downstream dependencies — not from dispatching a request to a handler.
For this reason, performance considerations are treated as a constraint rather than a goal. The focus remains on predictable behavior and clear semantics, with benchmarks serving as a guardrail rather than a headline feature.
More detailed information — including benchmarks, documented scenarios, and design constraints — is available in the project’s README. The full source code and ongoing discussions can be found in the Liaison.Mediator GitHub repository.
Who this approach is actually for
This approach is not meant for every .NET application or every team. It intentionally prioritizes explicitness and predictability over convenience and extensibility.
It tends to make sense in systems where startup behavior matters, where trimming or ahead-of-time compilation is a concern, or where understanding execution flow is more important than minimizing configuration code. Teams that value clear composition roots, stable semantics, and deliberate change boundaries are more likely to benefit from these trade-offs.
Conversely, applications that rely heavily on dynamic discovery, cross-cutting behaviors, or late-bound extensibility may find these constraints limiting rather than helpful. In those cases, more flexible mediator implementations are often a better fit.
If this resonates, the goal is not to convince you to switch libraries, but to offer a concrete example of how different priorities can lead to different, equally valid designs.
Top comments (1)
Happy to answer questions or clarify any of the design trade-offs discussed here.
I’m especially interested in hearing from people who’ve had to reason about mediator behavior under startup or trimming constraints.