DSoftStudio Mediator

← Back to Documentation

ADR-0001: Architecture Overview

Status

Released in v1.0.0

Overview

DSoftStudio.Mediator is an ultra-low-latency mediator for .NET with compile-time dispatch, zero-allocation pipelines, and a familiar MediatR-style API. This ADR documents all architectural decisions, design patterns, performance strategies, and trade-offs made during the development of the library.


1. Package Architecture

Decision

Split the library into three internal assemblies shipped as a single NuGet package:

Assembly Target Role
DSoftStudio.Mediator.Abstractions .NET Standard 2.0 + .NET 8 Public contracts (IRequest, INotification, IPipelineBehavior, etc.)
DSoftStudio.Mediator .NET 8 Runtime (mediator, caches, dispatch tables, publishers)
DSoftStudio.Mediator.Generators .NET Standard 2.0 Roslyn 4.12 incremental source generators

Rationale

Consequences


2. Compile-Time Dispatch via Source Generators

Decision

Use Roslyn incremental source generators to discover handlers, emit dispatch tables, and intercept Send()/Publish()/CreateStream() call sites at compile time.

Generators

Generator Purpose
SendInterceptorGenerator Intercepts ISender.Send<TRequest, TResponse>() call sites and replaces them with direct pipeline invocation via C# interceptors
PublishInterceptorGenerator Intercepts IPublisher.Publish<TNotification>() call sites
StreamInterceptorGenerator Intercepts ISender.CreateStream<TRequest, TResponse>() call sites
MediatorPipelineGenerator Discovers all IRequestHandler<,> implementations, generates MediatorRegistry for pipeline precompilation
NotificationGenerator Discovers all INotificationHandler<> implementations, generates NotificationRegistry
StreamGenerator Discovers all IStreamRequestHandler<,> implementations, generates StreamRegistry
DependencyInjectionGenerator Generates RegisterMediatorHandlers() extension method with auto-lifetime detection
MediatorExtensionsGenerator Generates typed extension methods for Send, Publish, CreateStream
HandlerDiscovery Shared helper for extracting handler type information from symbols
ReferencedAssemblyScanner Scans referenced assemblies for handler registrations (cross-project support)
InterceptorHelpers Shared helpers (ImplementsInterface, ResolveRequestParameter)

Rationale

Consequences


3. Static-Generic Dispatch Tables

Decision

Use static generic classes (RequestDispatch<TRequest, TResponse>, NotificationDispatch<TNotification>, StreamDispatch<TRequest, TResponse>) as write-once dispatch tables.

How it works

Rationale

Consequences


4. Zero-Allocation Pipeline via Interface Dispatch

Decision

PipelineChainHandler<TRequest, TResponse> passes this as the next parameter to each behavior via IRequestHandler<TRequest, TResponse> interface dispatch (virtual call), instead of using delegate closures.

How it works

Allocation profile

Rationale

Consequences


5. ThreadStatic Caching Strategy

Decision

Use [ThreadStatic] fields to cache handler and pipeline chain resolutions per thread.

Cache types

Cache Purpose Hit cost Miss cost
HandlerCache<TRequest, TResponse> Caches IRequestHandler on no-behaviors path ~1 ns ~10 ns (DI resolve)
PipelineChainCache<TRequest, TResponse> Caches PipelineChainHandler on behaviors path ~1 ns ~10 ns (DI resolve)
StreamPipelineChainCache<TRequest, TResponse> Caches StreamPipelineChainHandler ~1 ns ~10 ns (DI resolve)
StreamHandlerCache<TRequest, TResponse> Caches IStreamRequestHandler ~1 ns ~10 ns (DI resolve)
NotificationHandlerCache<TNotification> Caches resolved notification handler arrays ~1 ns ~10 ns (DI resolve)

Guard mechanism

Rationale

Consequences


6. Notification Dispatch by Exact Type

Decision

Notification dispatch is by exact compile-time type, not by inheritance hierarchy.

How it works

Rationale

Consequences


7. AOT and Trimming Compatibility

Decision

The library is fully compatible with .NET Native AOT publishing and IL trimming.

Implementation

Rationale

Consequences


8. Auto-Singleton Handler Registration

Decision

Stateless handlers (no constructor parameters) are automatically registered as Singleton. Handlers with DI dependencies are registered as Transient.

Rationale

Consequences


9. Pipeline Lifetime Determination

Decision

PrecompilePipelines() determines each PipelineChainHandler lifetime based on the registered components:

Components Chain Lifetime
All Singleton Singleton
Any Scoped Scoped
Any Transient Transient

Rationale

Consequences


10. Registration Order Contract

Decision

All service registrations must happen before the corresponding Precompile* call.

Required order

AddMediator()                → Core mediator services
RegisterMediatorHandlers()   → Source-generated handler registrations
[Register behaviors, processors, exception handlers, lifetime overrides]
PrecompilePipelines()        → Scans for IPipelineBehavior, Pre/Post processors, exception handlers
PrecompileNotifications()    → Builds static dispatch arrays for each INotification type
PrecompileStreams()           → Builds static factory delegates for each IStreamRequest type

Rationale

Consequences


11. Sync Fast-Path Optimization

Decision

All dispatch methods check IsCompletedSuccessfully before entering the async state machine.

How it works

Rationale

Consequences


12. Open-Generic Pipeline Behavior Support

Decision

Open-generic registrations (typeof(IPipelineBehavior<,>), typeof(IRequestPreProcessor<>), typeof(IRequestPostProcessor<,>), typeof(IRequestExceptionHandler<,>)) are detected at pipeline precompilation time via IsGenericTypeDefinition.

Rationale

Consequences


13. CQRS Support

Decision

Provide ICommand<TResponse>, IQuery<TResponse>, ICommandHandler, and IQueryHandler as semantic aliases for IRequest<TResponse> and IRequestHandler<TRequest, TResponse>.

Rationale

Consequences


14. Pluggable Notification Strategies

Decision

Notification dispatch strategy is configurable via INotificationPublisher.

Built-in strategies

Strategy Behavior
Sequential (default) Handlers run one at a time in registration order. If one throws, the rest are skipped.
ParallelNotificationPublisher All handlers start concurrently via Task.WhenAll. If any throw, AggregateException is raised after all complete.

Custom strategies

Users can implement INotificationPublisher for fire-and-forget, batched, prioritized, etc.

Rationale

Consequences


15. Multi-Project Support

Decision

Reference the source generator package only in the host project. Feature modules reference only the abstractions package.

Requirements

Rationale

Consequences


16. Performance Benchmarks (.NET 10, Isolated Runs)

Send (No Behaviors)

Method Latency Alloc Ratio
DirectCall 6.996 ns 72 B 1.00×
DSoft_Send 7.245 ns 72 B 1.04×

Send (Behaviors)

Method Latency Alloc Ratio
DirectCall 6.745 ns 72 B 1.00×
DSoft_Send_3Behaviors 13.820 ns 72 B 2.05×
DSoft_Send_5Behaviors 15.635 ns 72 B 2.32×

Cross-Library Comparison (All Libraries, Isolated Runs)

Operation DSoft Mediator SG DispatchR MediatR
Send() 7.2 ns 12.2 ns 33.4 ns 41.3 ns
Send() 5 beh 15.6 ns 36.8 ns 54.1 ns 153.1 ns
Publish() 4.5 ns 10.6 ns 35.7 ns 123.4 ns
CreateStream() 45.5 ns 44.7 ns 68.1 ns 122.9 ns
Cold Start 1.62 µs 9.91 µs 1.88 µs 3.24 µs

Allocation Comparison

Operation DSoft Mediator SG DispatchR MediatR
Send() 72 B 72 B 72 B 272 B
Send() 5 beh 72 B 72 B 72 B 1,088 B
Publish() 0 B 0 B 0 B 768 B
CreateStream() 232 B 232 B 232 B 624 B

Realistic Pipeline (Validation → Logging → Metrics → async DB)

Library Pipeline Latency Memory Overhead
DSoft Direct call 674 ns 271 B
  Mediator pipeline 667 ns 255 B 0.99×
DispatchR Direct call 661 ns 271 B
  Mediator pipeline 667 ns 255 B 1.01×
Mediator (SG) Direct call 679 ns 270 B
  Mediator pipeline 718 ns 397 B 1.06×
MediatR Direct call 714 ns 270 B
  Mediator pipeline 857 ns 1,032 B 1.20×

17. Code Quality & CI

Decision

Maintain strict code quality via SonarCloud, Roslyn analyzers, and performance regression tests.

Measures

Consequences


Document History

Date Version Changes
Draft Initial ADR covering all architectural decisions
2026-03-11 v1.0.0 Released with core mediator package