ModernMediator

A Comprehensive Tutorial

Version 0.2.2-alpha

Introduction

ModernMediator is a feature-rich mediator pattern library for .NET that facilitates decoupled communication between components in your applications. This tutorial will guide you through every major feature of the library, helping you understand when and how to use each capability.

By the end of this tutorial, you will understand:

Request/Response Pattern

Request/Response is the foundational pattern in ModernMediator. It models a simple interaction: you want something, and you expect an answer.

Request

A message object carrying the data needed to perform an operation.

Response

The result returned after the operation completes.

Mediator

The intermediary that routes the request to the appropriate handler.

How It Works

  1. Define a request class implementing IRequest<TResponse>
  2. Define a handler implementing IRequestHandler<TRequest, TResponse>
  3. The handler's Handle method receives the request and returns the response
  4. Call mediator.Send<TResponse>(request) to execute

Benefits

BenefitExplanation
DecouplingCaller doesn't know or care which class handles the request
Single ResponsibilityEach handler does exactly one thing
TestabilityHandlers are easy to mock and test in isolation
Cross-cutting concernsPipeline behaviors can wrap all requests

Streaming

Streaming allows a handler to return results incrementally rather than all at once. Instead of waiting for an entire collection to be built and returned, the consumer receives items as they become available.

In .NET, this is powered by IAsyncEnumerable<T>—a feature that lets you await foreach over items as they're yielded, one at a time.

How It Works

  1. Define a streaming request implementing IStreamRequest<TItem>
  2. Define a handler implementing IStreamRequestHandler<TRequest, TItem>
  3. The handler's method uses yield return to emit items
  4. Call mediator.CreateStream<TItem>(request) to get the IAsyncEnumerable<TItem>
  5. Consume with await foreach

Benefits

BenefitExplanation
Memory efficiencyOnly one item in memory at a time, not the entire collection
ResponsivenessConsumer sees first results immediately, doesn't wait for all
CancellationCan stop mid-stream without wasting work on remaining items
Natural fitIdeal for database cursors, file processing, real-time feeds

Key characteristic: Streaming is lazy—the handler doesn't run until the consumer starts iterating, and it pauses between yields while waiting for the consumer to request the next item. This creates natural backpressure.

Pub/Sub with Callbacks

In traditional Publish/Subscribe, a publisher broadcasts a message and multiple subscribers can receive it. It's fire-and-forget—the publisher doesn't know or care who handles the message, and no response comes back.

Pub/Sub with Callbacks extends this pattern by allowing subscribers to respond. The publisher still broadcasts to multiple subscribers, but now it can collect a response from each one.

This is a hybrid: the one-to-many broadcast of pub/sub combined with the response capability of request/response.

Pattern Comparison

PatternHandlersResponseUse Case
Request/ResponseOneSingleCommands, queries
Pub/Sub (fire-forget)ManyNoneEvents, notifications
Pub/Sub with CallbacksManyOne per handlerAggregation, voting, multi-source

Common Use Cases

ScenarioHow Callbacks Help
ValidationMultiple validators each return pass/fail, aggregate results
PricingMultiple pricing strategies respond, choose best or combine
SearchQuery multiple sources, merge results into unified response
Voting/consensusCollect opinions from multiple handlers, tally them
Health checksEach subsystem reports status, aggregate into overall health

DI and Scoping: When using dependency injection, IMediator is scoped—Pub/Sub subscriptions are per-scope. For application-wide shared subscriptions, use Mediator.Instance (the static singleton).

Pipeline Behaviors

Pipeline Behaviors are middleware for your mediator. They wrap around the entire request/response flow, allowing you to execute logic both before and after the handler runs—all within a single class.

Think of them like layers of an onion: the request passes through each behavior on the way in, hits the handler at the center, then passes back through each behavior on the way out.

How It Works

  1. Implement IPipelineBehavior<TRequest, TResponse>
  2. The Handle method receives the request and a next delegate
  3. Call await next() to continue the pipeline and get the response
  4. Add logic before and/or after the next() call

Common Use Cases

  • Logging — log before, log result after
  • Validation — reject bad requests before they reach the handler
  • Performance timing — start stopwatch before, stop after
  • Transaction management — begin before, commit/rollback after
  • Exception handling — wrap next() in try/catch

Key characteristic: Because you control when next() is called, you can short-circuit the pipeline entirely—returning early without ever reaching the handler.

Open vs Closed Generics: Open generic behaviors like LoggingBehavior<,> that apply to all requests must be registered explicitly with AddOpenBehavior(). Assembly scanning only discovers closed generic (request-specific) behaviors.

Pre/Post Processors

Pre/Post Processors are a simpler, more focused alternative to full pipeline behaviors. Instead of wrapping the handler, they run strictly before or strictly after it.

Processor TypeWhen It RunsWhat It Sees
Pre-ProcessorBefore the handlerRequest only
Post-ProcessorAfter the handlerRequest and Response

When to Use Processors vs Behaviors

ScenarioUse
Need logic both before AND afterPipeline Behavior
Need to short-circuit or modify the responsePipeline Behavior
Only need to run something beforePre-Processor
Only need to run something afterPost-Processor
Want simpler, single-purpose classesProcessors

Exception Handlers

Exception Handlers provide a centralized way to deal with exceptions that occur during request processing. Instead of wrapping every Send call in try/catch blocks throughout your application, you define exception handling logic once and it applies across your mediator pipeline.

How It Works

  1. Implement IExceptionHandler<TRequest, TResponse, TException>
  2. The handler receives the request, the exception, and can determine the outcome
  3. You can suppress the exception, rethrow it, return a fallback response, or transform it

Common Use Cases

Use CaseWhat the Handler Does
LoggingLog exception details, then rethrow
Fallback responsesReturn a default or cached value instead of failing
Exception translationConvert internal exceptions to user-friendly ones
Retry logicAttempt the operation again before giving up
MetricsRecord exception counts for monitoring, then rethrow

Source Generators

Source Generators are a C# compiler feature that generates additional code at compile time. Instead of discovering handlers and wiring things up at runtime using reflection, the generator inspects your code during compilation and writes the registration code for you.

The result: your application starts with all mediator wiring already baked in.

Benefits

BenefitExplanation
Zero reflectionNo runtime type scanning, no Assembly.GetTypes() calls
Faster startupRegistration is pre-computed, not discovered at runtime
Native AOT compatibleAOT compilation requires knowing all types ahead of time
Compile-time safetyErrors caught during build, not at runtime

Compile-Time Diagnostics

ModernMediator's source generator doesn't just wire things up—it validates your code and reports problems as compiler warnings or errors.

DiagnosticMeaningSeverity
MM001Request has no handler registeredWarning
MM002Request has multiple handlers (ambiguous)Error
MM003Handler implementation is invalid (signature issues)Error

Key characteristic: Traditional mediator libraries fail at runtime when a handler is missing. With compile-time diagnostics, you discover these problems before you even run the application—the build itself tells you something is wrong.

CachingMode (Eager vs Lazy)

CachingMode controls when ModernMediator resolves and caches handler instances. This affects both startup performance and first-request latency.

ModeWhen Handlers Are ResolvedTrade-off
EagerAt application startupSlower startup, faster first request
LazyOn first use of each handlerFaster startup, slower first request

When to Use Each

ScenarioRecommended Mode
Web API with predictable handler usageEager
Application with many rarely-used handlersLazy
Want startup validation of all dependenciesEager
Serverless/fast cold-start requirementsLazy
Desktop app where startup time is noticeableLazy

Weak/Strong References

When you subscribe a handler to the mediator, the mediator must hold a reference to that handler so it can invoke it later. The type of reference determines whether the handler can be garbage collected.

Reference TypeBehavior
StrongMediator keeps handler alive indefinitely
WeakHandler can be garbage collected if nothing else references it

In long-lived applications—especially UI applications—handlers are often tied to objects with shorter lifespans (views, view models, components). If the mediator holds strong references, those objects can never be garbage collected, causing memory leaks.

When to Use Each

ScenarioRecommended
Handler is a long-lived singleton serviceStrong
Handler is tied to a UI component's lifetimeWeak
Handler should live as long as the appStrong
Handler is on a view that opens and closesWeak

Predicate Filters

Predicate filters allow a subscriber to conditionally receive messages based on the message content. Instead of receiving every published message of a given type, the handler only receives messages that pass its filter.

When subscribing, you provide a predicate function—a condition that evaluates the message and returns true or false. The handler is only invoked when the predicate returns true.

Example Scenarios

Message TypePredicateResult
OrderPlacedorder => order.Total > 1000Only handles high-value orders
UserLoggedInuser => user.IsAdminOnly handles admin logins
SensorReadingreading => reading.Value > 100Only handles threshold breaches

Key characteristic: Predicates are evaluated before invocation. A handler with a predicate that returns false is skipped entirely—it incurs no invocation overhead.

Covariance

Covariance allows a handler subscribed to a base type to receive messages of derived types. It's inheritance-aware message routing.

If you have a class hierarchy (Animal with Dog and Cat as subclasses), a handler subscribed to Animal will receive Dog messages, Cat messages, and Animal messages. The subscription covers the entire hierarchy below it.

Practical Examples

Handler Subscribes ToReceives
AnimalAnimal, Dog, Cat, any future subclasses
DogDog only
ExceptionAny exception type
EntityChangedCustomerChanged, OrderChanged, etc.

Covariance enables powerful patterns: a logging handler subscribed to a base event type logs all events; an audit handler for EntityChanged captures all entity modifications; a base exception handler catches anything more specific handlers miss.

UI Dispatchers

UI frameworks have a fundamental rule: only the main thread (the UI thread) can update the user interface. When work happens on a background thread—as it often does with async handlers—the results must be marshaled back to the UI thread before updating any controls.

Dispatchers automatically route handler responses back to the appropriate thread. Your handlers can run on any thread, but when they complete, the dispatcher ensures the result arrives on the UI thread, ready to bind to your views.

Platform-Specific Threading Models

PlatformThreading MechanismUI Thread Concept
WPFDispatcher.InvokeSingle UI thread
WinFormsControl.Invoke / SynchronizationContextSingle UI thread
MAUIMainThread.BeginInvokeOnMainThreadSingle main thread
AvaloniaDispatcher.UIThread.InvokeSingle UI thread
ASP.NET CoreNo UI threadRequest context

Benefits

Without DispatchersWith Dispatchers
Manual Dispatcher.Invoke calls everywhereAutomatic thread marshaling
Easy to forget and cause crashesConsistent, safe by default
Handler code cluttered with UI concernsHandlers stay pure and focused
Platform-specific code in handlersPlatform abstracted away

Assembly Scanning and Manual DI Registration

Scoped by Default: IMediator is registered as Scoped, allowing handlers to resolve scoped dependencies like DbContext. For shared Pub/Sub subscriptions across the application, use Mediator.Instance (the static singleton).

Assembly Scanning

Assembly scanning is automatic discovery. At startup, ModernMediator scans your compiled assemblies, finds all classes implementing handler interfaces, and registers them with the dependency injection container automatically.

BenefitExplanation
ConvenienceAdd a handler class, it's automatically registered
No forgotten handlersNew handlers can't be accidentally left out of registration
Less boilerplateNo growing list of services.AddTransient<...>() calls
Refactoring safeRename or move handlers without updating registration code

Manual DI Registration

Manual registration is explicit wiring. You write code that specifically registers each handler with the dependency injection container. Nothing is automatic—every handler appears in your configuration.

BenefitExplanation
ExplicitConfiguration shows exactly what's registered
Full controlDifferent lifetimes, conditional registration, ordering
No surprisesOnly intentionally registered handlers participate
Faster startupNo reflection-based scanning overhead

Choosing Between Them

ScenarioRecommended Approach
Rapid development, handlers change frequentlyAssembly scanning
Small project with few handlersManual registration
Need different lifetimes per handlerManual registration
Large team, handlers spread across projectsAssembly scanning
Native AOT deploymentSource generators (hybrid)
Want compile-time validationSource generators (hybrid)

Async-First Design

ModernMediator is built from the ground up for async operations. Async isn't an afterthought or an adapter over synchronous code—it's the primary execution model.

True Async Handlers

When you use SubscribeAsync, handlers are genuinely asynchronous. When multiple handlers exist for a notification, they execute concurrently using Task.WhenAll. The mediator awaits all handlers together, not one after another.

Approach3 Handlers Taking 1 Second EachTotal Time
Sequential awaitAwait handler 1, then 2, then 3~3 seconds
Task.WhenAllAwait all simultaneously~1 second

Cancellation Support

All async operations in ModernMediator accept and respect CancellationToken. When a cancellation is requested, handlers can exit early and the mediator propagates the cancellation appropriately.

ScenarioWithout CancellationWith Cancellation
User navigates away mid-requestHandler keeps running, wastes resourcesHandler exits cleanly
HTTP request times outBackground work continuesWork stops promptly
Application shutdownHandlers may hang shutdownHandlers cooperate with shutdown

Parallel Execution

When a notification is published to multiple subscribers, handlers execute concurrently rather than sequentially. This maximizes throughput for independent operations.

Error Handling

Beyond IExceptionHandler for custom exception handling logic, ModernMediator also provides policies and events for broader error management.

Three Error Policies

Error policies define how the mediator behaves when a handler throws an exception, especially in multi-handler scenarios like pub/sub.

PolicyBehavior
ContinueAndAggregateExecute all handlers, collect all exceptions, throw as aggregate
StopOnFirstErrorStop immediately when any handler throws, skip remaining
LogAndContinueLog exceptions, continue executing remaining handlers, don't throw

HandlerError Event

A hook that fires whenever a handler throws an exception, regardless of the error policy. This provides a centralized point for logging and monitoring without requiring custom exception handlers.

Exception Unwrapping

When handlers are invoked through reflection or dynamic dispatch, exceptions get wrapped in TargetInvocationException or AggregateException. Stack traces become cluttered with framework noise, making debugging harder.

Exceptions are automatically unwrapped before being thrown or passed to exception handlers. You see the original exception with a clean stack trace pointing directly to your code.

Summary

ModernMediator provides a comprehensive set of features for building decoupled, maintainable applications in .NET. This tutorial has covered the core capabilities that make the library powerful and flexible.

Features Covered

  • Request/Response Pattern — The foundational one-to-one message handling pattern for commands and queries
  • Pipeline Behaviors — Middleware that wraps handler execution for cross-cutting concerns
  • Pre/Post Processors — Simpler alternatives to behaviors when you only need before or after logic
  • Streaming — Memory-efficient, responsive data delivery using IAsyncEnumerable
  • Source Generators — Compile-time handler discovery for Native AOT compatibility
  • Exception Handlers — Centralized exception handling without scattered try/catch blocks
  • CachingMode — Control over handler resolution timing with Eager and Lazy options
  • Pub/Sub with Callbacks — Multi-handler broadcasting with response aggregation
  • Weak/Strong References — Memory management control for subscription lifecycles
  • Predicate Filters — Conditional message reception based on content
  • Covariance — Inheritance-aware message routing for flexible subscriptions
  • UI Dispatchers — Thread-safe response delivery across WPF, WinForms, MAUI, Avalonia, and ASP.NET Core
  • Assembly Scanning and Manual DI Registration — Flexible handler registration strategies
  • Async-First Design — True parallelism with Task.WhenAll and full cancellation support
  • Error Handling — Configurable policies, centralized events, and clean exception unwrapping

ModernMediator combines these features into a cohesive library that supports applications from simple desktop tools to complex distributed systems, with first-class support for modern .NET capabilities including Native AOT compilation.

For more information, visit: https://github.com/EvanscoApps/ModernMediator