Appouse.Mediator 1.0.0

dotnet add package Appouse.Mediator --version 1.0.0
                    
NuGet\Install-Package Appouse.Mediator -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Appouse.Mediator" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Appouse.Mediator" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Appouse.Mediator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Appouse.Mediator --version 1.0.0
                    
#r "nuget: Appouse.Mediator, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Appouse.Mediator@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Appouse.Mediator&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Appouse.Mediator&version=1.0.0
                    
Install as a Cake Tool

Appouse.Mediator

A small, fast, dependency-injection friendly mediator for .NET.

Requests and responses, notifications, streaming requests, pipeline behaviors, pre/post processors and exception handling — with no dependency beyond Microsoft.Extensions.DependencyInjection.Abstractions.

Targets .NET 5, 6, 7, 8, 9 and 10.

Türkçe dokümantasyon: docs/README.tr.md

Install

dotnet add package Appouse.Mediator

Quick start

using Appouse.Mediator;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddAppouseMediator(configuration =>
    configuration.RegisterServicesFromAssemblyContaining<GetGreeting>());

var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();

Console.WriteLine(await mediator.Send(new GetGreeting("Mustafa")));

public record GetGreeting(string Name) : IRequest<string>;

public sealed class GetGreetingHandler : IRequestHandler<GetGreeting, string>
{
    public Task<string> Handle(GetGreeting request, CancellationToken cancellationToken)
        => Task.FromResult($"Merhaba, {request.Name}!");
}

In ASP.NET Core, call builder.Services.AddAppouseMediator(...) and inject the mediator into your endpoints or controllers. Inject ISender when you only send requests and IPublisher when you only publish notifications; IMediator gives you both.

A runnable end-to-end sample lives in samples/Appouse.Mediator.Sample.

Requests

One request, exactly one handler. A second handler for the same request is reported as an error while AddAppouseMediator runs, rather than one of them silently winning.

public record GetProduct(int Id) : IRequest<Product>;

public sealed class GetProductHandler : IRequestHandler<GetProduct, Product>
{
    public Task<Product> Handle(GetProduct request, CancellationToken cancellationToken) => ...;
}

var product = await sender.Send(new GetProduct(42));

A request that produces nothing implements IRequest, and its handler returns a plain Task:

public record DeleteProduct(int Id) : IRequest;

public sealed class DeleteProductHandler : IRequestHandler<DeleteProduct>
{
    public Task Handle(DeleteProduct request, CancellationToken cancellationToken) => ...;
}

await sender.Send(new DeleteProduct(42));   // returns Task, not Task<Unit>

When the request type is only known at runtime, use the object overload — the response comes back boxed as object? (Unit for a void request):

object? response = await sender.Send(someRequestObject, cancellationToken);

Streaming requests

public record CountTo(int Limit) : IStreamRequest<int>;

public sealed class CountToHandler : IStreamRequestHandler<CountTo, int>
{
    public async IAsyncEnumerable<int> Handle(
        CountTo request,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        for (var i = 1; i <= request.Limit; i++)
        {
            await Task.Delay(50, cancellationToken);
            yield return i;
        }
    }
}

await foreach (var tick in sender.CreateStream(new CountTo(3)))
{
    Console.WriteLine(tick);
}

Streams have their own pipeline: IStreamPipelineBehavior<TRequest, TResponse>. A regular IPipelineBehavior does not apply to CreateStream.

Notifications

Zero or more handlers, no response.

public record ProductViewed(int Id) : INotification;

public sealed class SendEmail : INotificationHandler<ProductViewed>
{
    public Task Handle(ProductViewed notification, CancellationToken cancellationToken) => ...;
}

// For synchronous work, derive from NotificationHandler<T> instead.
public sealed class UpdateMetrics : NotificationHandler<ProductViewed>
{
    protected override void Handle(ProductViewed notification) => ...;
}

await publisher.Publish(new ProductViewed(42));

Publishing strategies

Publisher Behavior
ForeachAwaitPublisher Default. Awaits each handler in turn; the first failure stops the rest and propagates.
TaskWhenAllPublisher Starts every handler at once and awaits them together. Handlers must be safe to run concurrently — in particular they must not share a scoped DbContext.
SequentialAllPublisher Awaits each handler in turn but keeps going after a failure, then throws an AggregateException holding every error.
configuration.UseNotificationPublisher<TaskWhenAllPublisher>();

Implement INotificationPublisher for anything else — ordering, batching, fan-out to a queue.

Base type handlers

The container does not honour the contravariance of INotificationHandler<in T>, so by default publishing OrderPlaced reaches only handlers of OrderPlaced, not handlers of its base type. Opt into polymorphic publishing when you want both:

configuration.PublishToBaseNotificationHandlers = true;

Base classes and notification interfaces are then included as well, and each handler type runs at most once per publish.

Pipeline behaviors

A behavior wraps the handler and everything registered after it:

public sealed class TimingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var started = Stopwatch.GetTimestamp();
        try
        {
            return await next(cancellationToken);
        }
        finally
        {
            Log(typeof(TRequest), Stopwatch.GetElapsedTime(started));
        }
    }
}

configuration.AddOpenBehavior(typeof(TimingBehavior<,>));

Not calling next short-circuits the handler, which is how caching or authorisation behaviors work. next() may be called without arguments; pass a token only when you deliberately want to replace the one the request was sent with — a timeout behavior, for instance.

Order

Behaviors nest in registration order. From outermost to innermost:

# Layer Registered when
1 RequestExceptionActionProcessorBehavior any IRequestExceptionAction exists
2 RequestExceptionProcessorBehavior any IRequestExceptionHandler exists
3 Behaviors from AddBehavior / AddOpenBehavior in the order you called them
4 Closed behaviors found by scanning AutoRegisterBehaviorsFromAssemblies is true (default), ordered by full type name
5 RequestPreProcessorBehavior any IRequestPreProcessor exists
6 RequestPostProcessorBehavior any IRequestPostProcessor exists
7 Your IRequestHandler always

Set AutoRegisterBehaviorsFromAssemblies = false when you want the pipeline to be exactly what you listed and nothing else. Open generic behaviors are never picked up by scanning — applying one to every message in the application is too big a decision to infer, so name it with AddOpenBehavior.

Pre and post processors

Lighter than a behavior when you only need to run something before or after the handler:

public sealed class ValidateProduct : IRequestPreProcessor<CreateProduct>
{
    public Task Process(CreateProduct request, CancellationToken cancellationToken) => ...;
}

public sealed class PublishAudit : IRequestPostProcessor<CreateProduct, int>
{
    public Task Process(CreateProduct request, int response, CancellationToken cancellationToken) => ...;
}

Closed processors are picked up by scanning. Post-processors do not run when the handler throws. Generic ones apply to every request and are opted into by name:

configuration.AddOpenRequestPreProcessor(typeof(AuditPreProcessor<>));
configuration.AddOpenRequestPostProcessor(typeof(AuditPostProcessor<,>));

Exception handling

IRequestExceptionHandler can swallow an exception and supply a response instead:

public sealed class GetProductNotFound
    : IRequestExceptionHandler<GetProduct, Product, ProductMissingException>
{
    public Task Handle(
        GetProduct request,
        ProductMissingException exception,
        RequestExceptionHandlerState<Product> state,
        CancellationToken cancellationToken)
    {
        state.SetHandled(Product.Empty);
        return Task.CompletedTask;
    }
}

Handlers are matched by walking the thrown exception's type hierarchy from the most derived type up to Exception, so a handler registered against a base exception type still sees derived ones. The first handler to call SetHandled wins; if none does, the original exception is rethrown with its stack trace intact.

IRequestExceptionAction observes a failure without claiming it — logging, metrics, compensation — and the exception always propagates afterwards. RequestExceptionHandler<,,> and RequestExceptionAction<,> are synchronous base classes for both.

By default actions only run for exceptions that no handler swallowed. To run them for every failure:

configuration.RequestExceptionActionProcessorStrategy =
    RequestExceptionActionProcessorStrategy.ApplyForAllExceptions;

Configuration reference

services.AddAppouseMediator(configuration =>
{
    configuration.RegisterServicesFromAssemblyContaining<Program>();
    configuration.RegisterServicesFromAssembly(typeof(SomeHandler).Assembly);

    configuration.Lifetime = ServiceLifetime.Transient;      // default
    configuration.TypeEvaluator = type => !type.IsNested;    // filter what gets scanned

    configuration.AddBehavior<AuthorisationBehavior>();
    configuration.AddOpenBehavior(typeof(TimingBehavior<,>));
    configuration.AddOpenStreamBehavior(typeof(StreamTimingBehavior<,>));
    configuration.AddRequestPreProcessor<ValidateProduct>();
    configuration.AddOpenRequestPostProcessor(typeof(AuditPostProcessor<,>));

    configuration.UseNotificationPublisher<TaskWhenAllPublisher>();
    configuration.PublishToBaseNotificationHandlers = true;
    configuration.AutoRegisterBehaviorsFromAssemblies = false;
    configuration.MediatorImplementationType = typeof(MyMediator);
});

There is also a shorthand that scans assemblies with all defaults:

services.AddAppouseMediator(typeof(Program).Assembly);

Lifetime applies to everything found by scanning and to IMediator itself. The built-in pipeline behaviors are always transient so they cannot capture a shorter-lived dependency.

Things worth knowing

Handlers you register yourself win. Scanned handlers are added with TryAdd, so registering IRequestHandler<T, TResponse> before calling AddAppouseMediator overrides the scanned one. This is also how you use an open generic handler, which scanning deliberately leaves alone:

services.AddTransient<IRequestHandler<Query<int>, int>, QueryHandler<int>>();

Native AOT is not supported. Dispatch builds a statically typed wrapper per message type with MakeGenericType, which needs a runtime that can generate code. The wrapper is cached, so the reflection cost is paid once per message type and never on the hot path.

Customising the mediator. Derive from Mediator, override PublishCore, and point MediatorImplementationType at your subclass — useful for an outbox, or for logging every publish.

Cancellation. The token you pass to Send flows to every behavior, processor and handler. Inside a behavior, next() forwards it automatically; next(myToken) replaces it.

Building from source

dotnet build -c Release      # net5.0 through net10.0
dotnet test
dotnet run --project samples/Appouse.Mediator.Sample
dotnet pack src/Appouse.Mediator/Appouse.Mediator.csproj -c Release -o artifacts

Building the .NET 5/6/7 targets downloads the matching reference packs from NuGet; the .NET 10 SDK is all you need installed.

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 101 7/27/2026