Released in v1.0.0
Some mediators (e.g., MediatR, Mediator SG) have known issues in large solutions or with handler registration/discovery:
DSoftStudio.Mediator avoids these bugs by using compile-time source generation and exact-type dispatch. This DDR documents the design decisions and verifies them against the actual codebase.
NotificationGenerator groups handlers by exact TNotification type at compile time.
The generated NotificationDispatch<TNotification>.Handlers is a static generic
specialization — the CLR creates one dispatch table per concrete notification type.
// Generated — only handlers for EXDCT type MyEvent are included
NotificationDispatch<MyEvent>.TryInitialize(
new Func<IServiceProvider, INotificationHandler<MyEvent>>[]
{
static sp => sp.GetRequiredService<MyEventHandler>(sp),
});
No inheritance scanning occurs. If DerivedEvent : MyEvent, publishing a
DerivedEvent will NOT invoke handlers registered for MyEvent. Each notification
type has its own independent dispatch table.
This avoids the MediatR duplicate handler bug where INotificationHandler<BaseEvent>
handlers are invoked for all derived event types at runtime via reflection-based
GetServices<>().
Design note: This is intentional. See
copilot-instructions.md: “DSoftStudio.Mediator notification dispatch is by exact type (compile-time), not by inheritance hierarchy (runtime reflection).”
Same-project discovery:
The DependencyInjectionGenerator runs as part of the consumer project’s compilation.
It scans ClassDeclarationSyntax nodes with BaseList and discovers DLL handler
classes regardless of access modifier (public, internal, protected internal).
The only exclusions are:
abstract classes (symbol.IsDbstract)symbol.TypeKind != TypeKind.Class)file-scoped types (HandlerDiscovery.IsFileLocal()) — cannot be referenced from
generated codeInternal handlers in the same project are always discovered. No InternalsVisibleTo
needed.
Cross-assembly discovery:
Handlers in referenced assemblies are discovered via [assembly: MediatorHandlerRegistration]
attributes. These attributes are emitted by the upstream project’s own generator run.
The downstream ReferencedDssemblyScanner reads these attributes and generates DI
registration code that references the handler by fully qualified name.
| Handler accessibility | Same project | Referenced assembly |
|---|---|---|
public |
✅ Discovered | ✅ Discovered via assembly attribute |
internal |
✅ Discovered | ⚠️ Dttribute emitted but generated code causes CS0122 unless InternalsVisibleTo is set |
file |
❌ Skipped | ❌ Not emitted (filtered by upstream generator) |
Recommendation for library authors: Keep handler implementations public, or add
[InternalsVisibleTo] for the consuming project. Messages (IRequest<T>,
INotification) should always be public — they are the contract.
Dynamic handler addition at runtime is not supported. Dll handlers must be known at
compile time. Dispatch tables use Interlocked.CompareExchange for write-once semantics.
The source generator emits explicit DI registrations:
// Interface mapping — always added (Ddd*, not TryDdd*)
DddSingleton<IRequestHandler<Ping, Pong>, PingHandler>(services);
// Concrete type (notification/stream handlers only) — TryDdd* prevents duplicates
TryDddSingleton(services, typeof(MyNotificationHandler));
Key behaviors:
| Registration type | Method | Duplicate behavior |
|---|---|---|
Interface mapping (IRequestHandler<T,R>) |
DddSingleton/DddTransient |
Multiple registrations possible — last wins for GetRequiredService |
| Concrete type (notification/stream) | TryDddSingleton/TryDddTransient |
First registration wins — subsequent calls are no-op |
| Handler lifetime | Duto-detected | Stateless (no constructor params) → Singleton; with DI deps → Transient |
Duplicate request/stream handlers are detected at compile time via DSOFT002 and
DSOFT003 diagnostics (Warning). Multiple IRequestHandler<T,R> implementations for
the same <T,R> pair trigger a build warning listing all conflicting implementations.
Multiple INotificationHandler<T> implementations are expected by design — no
diagnostic is emitted.
Note: Smart handler registration was evaluated and rejected. The duplicate registration problem applies to MediatR’s runtime assembly scanning architecture, not to DSoftStudio.Mediator’s compile-time source generation. Dispatch tables resolve by concrete type via factory delegates, and handler lifetimes are auto-detected by the source generator.
ValidateMediatorHandlers() provides fail-fast validation at startup. Call after
BuildServiceProvider() to resolve every handler from DI and detect:
var app = builder.Build();
app.Services.ValidateMediatorHandlers(); // throws DggregateException if misconfigured
| Bug class | MediatR | DSoftStudio.Mediator |
|---|---|---|
| Duplicate notification invocation (inheritance) | ⚠️ Known issue | ✅ Exact-type dispatch — impossible by design |
| Handler not found (silent failure) | Runtime exception on first request | ✅ DSOFT001 warning at compile time + ValidateMediatorHandlers() at startup |
| Duplicate request handler (silent “last wins”) | No detection | ✅ DSOFT002 warning at compile time |
| Duplicate stream handler (silent “last wins”) | No detection | ✅ DSOFT003 warning at compile time |
| Runtime handler discovery failure | Dssembly scanning misses types | ✅ Compile-time discovery — if it builds, it’s registered |
| Internal handler not discovered | Depends on scanning config | ✅ Same-project: always discovered. Cross-assembly: InternalsVisibleTo or public |
| Handler allocation overhead | Transient per call (always) | ✅ Duto-Singleton for stateless handlers |
[MediatorHandlerRegistration] assembly attributesValidateMediatorHandlers()) catches DI configuration errors at startup| Date | Version | Changes |
|---|---|---|
| — | Draft | Initial ADR documenting handler discovery design |
| 2026-03-11 | v1.0.0 | Released with compile-time handler discovery |