Appouse.Mediator
1.0.0
dotnet add package Appouse.Mediator --version 1.0.0
NuGet\Install-Package Appouse.Mediator -Version 1.0.0
<PackageReference Include="Appouse.Mediator" Version="1.0.0" />
<PackageVersion Include="Appouse.Mediator" Version="1.0.0" />
<PackageReference Include="Appouse.Mediator" />
paket add Appouse.Mediator --version 1.0.0
#r "nuget: Appouse.Mediator, 1.0.0"
#:package Appouse.Mediator@1.0.0
#addin nuget:?package=Appouse.Mediator&version=1.0.0
#tool nuget:?package=Appouse.Mediator&version=1.0.0
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 | Versions 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. |
-
net10.0
-
net5.0
-
net6.0
-
net7.0
-
net8.0
-
net9.0
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 |