Skip to content

Dependency Injection

The Nerdigy.Mediator.DependencyInjection package connects Nerdigy.Mediator to Microsoft.Extensions.DependencyInjection. A single call to AddMediator scans your assemblies, discovers every handler and pipeline component, and registers them with the correct lifetime and cardinality -- no manual wiring required.

INFO

The core Nerdigy.Mediator package has no dependency on any DI framework. Nerdigy.Mediator.DependencyInjection is an optional add-on that provides the assembly scanning and service registration shown on this page.

Basic Setup

Pass a configuration callback to AddMediator and tell it which assemblies to scan.

csharp
services.AddMediator(options =>
{
    options.RegisterServicesFromAssemblyContaining<CreateOrderHandler>();
});

RegisterServicesFromAssemblyContaining<T>() resolves the assembly that contains the marker type T and queues it for scanning. Any concrete, non-abstract class in that assembly implementing a mediator interface is registered automatically.

After calling AddMediator, resolve IMediator, ISender, or IPublisher from the container as usual:

csharp
var mediator = serviceProvider.GetRequiredService<IMediator>();

Multiple Assemblies

When handlers and pipeline components live across several projects, register each assembly explicitly.

csharp
services.AddMediator(options =>
{
    options.RegisterServicesFromAssemblies(
        typeof(CreateOrderHandler).Assembly,
        typeof(BillingNotificationHandler).Assembly);
});

A shorthand overload accepts assemblies directly, without the options callback:

csharp
services.AddMediator(
    typeof(CreateOrderHandler).Assembly,
    typeof(BillingNotificationHandler).Assembly);

WARNING

AddMediator throws an InvalidOperationException if no assemblies are configured. At least one assembly must be registered for scanning.

What Gets Registered

The scanner inspects every concrete, non-abstract class in the configured assemblies and registers any type that implements a recognized mediator interface. The interfaces fall into two categories based on their registration semantics.

Single Registration (one handler per request type)

These interfaces use TryAdd, which registers only the first implementation found for a given service type. A second handler for the same request type is silently ignored.

InterfacePurpose
IRequestHandler<TRequest, TResponse>Handles a request and returns a response
IRequestHandler<TRequest>Handles a void request (no response)
IStreamRequestHandler<TRequest, TResponse>Handles a stream request returning IAsyncEnumerable<TResponse>

This enforces the mediator contract: each request type maps to exactly one handler.

Multi Registration (multiple components per type)

These interfaces use TryAddEnumerable, which allows multiple implementations for the same service type while preventing exact duplicates.

InterfacePurpose
INotificationHandler<TNotification>Handles a notification (fan-out)
IPipelineBehavior<TRequest, TResponse>Request pipeline middleware
IStreamPipelineBehavior<TRequest, TResponse>Stream pipeline middleware
IRequestPreProcessor<TRequest>Runs before the request handler
IRequestPostProcessor<TRequest, TResponse>Runs after the request handler
IRequestExceptionHandler<TRequest, TResponse, TException>Handles exceptions with recovery option
IRequestExceptionAction<TRequest, TException>Side-effect on exception before rethrow
IStreamRequestExceptionHandler<TRequest, TResponse, TException>Handles stream request exceptions

Notifications need multiple handlers by design. Pipeline components stack: every registered behavior, processor, or exception handler participates in the pipeline for its matching request type.

Open-Generic Registration

The scanner supports open-generic types for all multi-registration interfaces. Define a generic pipeline component once and it applies to every matching request type without per-type wiring.

csharp
public sealed class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // log before
        TResponse response = await next();
        // log after

        return response;
    }
}

Place this class in a scanned assembly and the scanner registers IPipelineBehavior<,> mapped to LoggingBehavior<,>. The DI container closes the generic at resolve time for each concrete request type.

For explicit behavior ordering in the options block, register open behaviors directly:

csharp
services.AddMediator(options =>
{
    options.RegisterServicesFromAssemblyContaining<CreateOrderHandler>();
    options.AddOpenBehavior(typeof(ValidationBehavior<,>));
    options.AddOpenBehavior(typeof(LoggingBehavior<,>));
});

The same pattern works for IRequestPreProcessor<>, IRequestPostProcessor<,>, IStreamPipelineBehavior<,>, exception handlers, and exception actions.

Service Lifetimes

Two properties on NerdigyMediatorOptions control lifetimes:

PropertyControlsDefault
MediatorLifetimeIMediator, ISender, IPublisherTransient
HandlerLifetimeAll scanned handlers and pipeline componentsTransient

Override them in the configuration callback:

csharp
services.AddMediator(options =>
{
    options.RegisterServicesFromAssemblyContaining<CreateOrderHandler>();
    options.MediatorLifetime = ServiceLifetime.Scoped;
    options.HandlerLifetime = ServiceLifetime.Scoped;
});

TIP

If your handlers inject scoped services (like DbContext), set HandlerLifetime to Scoped to match their scope. Resolving a scoped dependency from a transient service works in some containers but produces subtle lifetime bugs in others.

Notification Publisher Strategy

By default, notifications are published sequentially using ForeachAwaitPublisher, which awaits each handler one at a time. Switch to parallel execution with TaskWhenAllPublisher:

csharp
services.AddMediator(options =>
{
    options.RegisterServicesFromAssemblyContaining<CreateOrderHandler>();
    options.UseNotificationPublisherStrategy(
        NerdigyMediatorNotificationPublisherStrategy.Parallel);
});

The two built-in strategies:

StrategyBehavior
Sequential (default)Awaits each INotificationHandler<T> in sequence
ParallelStarts all handlers concurrently via Task.WhenAll

Custom Publisher

For full control over dispatch ordering, throttling, or resilience, implement INotificationPublisher and register it by type or instance:

csharp
// Register by type (resolved from the container)
options.UseNotificationPublisher<MyCustomPublisher>();

// Register a specific instance (singleton)
options.UseNotificationPublisher(new MyCustomPublisher());

When registering by instance, the publisher is registered as a singleton.

Core Service Registrations

AddMediator registers the following core services in addition to scanned components:

ServiceImplementationLifetime
IMediatorMediatorMediatorLifetime
ISenderForwarded to IMediatorMediatorLifetime
IPublisherForwarded to IMediatorMediatorLifetime
INotificationPublisherConfigured publisher typeSingleton

All core registrations use TryAdd, so calling AddMediator multiple times does not produce duplicate registrations. The first call wins.

Mediator runtime for .NET