PediatR 1.4.0
dotnet add package PediatR --version 1.4.0
NuGet\Install-Package PediatR -Version 1.4.0
<PackageReference Include="PediatR" Version="1.4.0" />
<PackageVersion Include="PediatR" Version="1.4.0" />
<PackageReference Include="PediatR" />
paket add PediatR --version 1.4.0
#r "nuget: PediatR, 1.4.0"
#:package PediatR@1.4.0
#addin nuget:?package=PediatR&version=1.4.0
#tool nuget:?package=PediatR&version=1.4.0
PediatR
PediatR is a small, dependency-light mediator library for .NET whose public API is a
source-compatible reimplementation of the MediatR 12.5.0
contract, under the PediatR namespace instead of MediatR.
It is written from scratch (clean-room): only the shape of the public API — interface names, method signatures, generic variance and observable behavior of the mediator pattern — is mirrored. No MediatR implementation code is used. PediatR ships under the permissive MIT license.
The mediator pattern belongs to nobody. PediatR reproduces a familiar API contract so you can adopt it with a mechanical find-and-replace, and keep the rest of your code unchanged.
Why
If you have code written against MediatR and want a drop-in replacement, replace the string
MediatR with PediatR in two places:
- the package reference (
MediatR→PediatR), and - the namespace in your
usingdirectives (using MediatR;→using PediatR;).
Everything else — your IRequest<T>, IRequestHandler<,>, INotification, IPipelineBehavior<,>,
IStreamRequestHandler<,>, pre/post processors, exception handlers, and the
services.AddMediatR(...) → services.AddPediatR(...) registration call — keeps compiling and
behaving the same way.
The DI entry points live in the Microsoft.Extensions.DependencyInjection namespace (unchanged), so
AddMediatR becomes AddPediatR and MediatRServiceConfiguration becomes
PediatRServiceConfiguration through the same find-and-replace.
Install
dotnet add package PediatR
Multi-targets netstandard2.0 (broad reach, incl. .NET Framework), net8.0, net9.0 and net10.0,
so consumers on the latest runtimes get a natively-built, optimized asset.
Quick start
using PediatR;
using Microsoft.Extensions.DependencyInjection;
// 1. Define a request and its handler
public record Ping(string Message) : IRequest<string>;
public class PingHandler : IRequestHandler<Ping, string>
{
public Task<string> Handle(Ping request, CancellationToken cancellationToken)
=> Task.FromResult($"{request.Message} Pong");
}
// 2. Register PediatR, pointing it at the assemblies to scan
var services = new ServiceCollection();
services.AddPediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Ping>());
var provider = services.BuildServiceProvider();
// 3. Send
var mediator = provider.GetRequiredService<IMediator>();
string response = await mediator.Send(new Ping("Ping")); // "Ping Pong"
What's included
- Sending:
ISender/IMediator.Send<TResponse>(IRequest<TResponse>), the voidSend<TRequest>(TRequest)forIRequest, and the runtime-typedSend(object). - Requests & handlers:
IRequest,IRequest<TResponse>,IBaseRequest,IRequestHandler<,>,IRequestHandler<>, and theUnittype. - Notifications:
INotification,INotificationHandler<>, theNotificationHandler<>base class, and publish strategiesForeachAwaitPublisher(sequential) andTaskWhenAllPublisher(concurrent), selectable viaINotificationPublisher/NotificationHandlerExecutor. - Streaming:
IStreamRequest<TResponse>,IStreamRequestHandler<,>,IStreamPipelineBehavior<,>andIMediator.CreateStream(...)overIAsyncEnumerable<T>. - Pipeline behaviors:
IPipelineBehavior<,>withRequestHandlerDelegate<TResponse>. - Pre/post processing:
IRequestPreProcessor<>,IRequestPostProcessor<,>and their behaviors. - Exception handling:
IRequestExceptionHandler<,,>,IRequestExceptionAction<,>,RequestExceptionHandlerState<TResponse>, and theRequestExceptionActionProcessorStrategy. - Registration:
AddPediatR(...), the fullPediatRServiceConfigurationsurface (RegisterServicesFromAssembly*,AddBehavior,AddOpenBehavior(s),AddStreamBehavior,AddRequestPreProcessor,AddRequestPostProcessor,Lifetime,NotificationPublisher(Type), …).
Source-generated handlers (optional)
Beyond the classic hand-written IRequestHandler<,>, PediatR ships an optional Roslyn source
generator — bundled in the same package, no extra install — that removes the request/handler
boilerplate. Annotate an instance method and the generator emits the matching request type and
handler for you. The generated request keeps the exact MediatR shape, so it runs through the same
pipeline as everything else and is picked up by the same assembly scan (no extra discovery marker).
Three authoring modes, one pipeline:
public sealed class TodoFeatures(AppDbContext db)
{
// Neutral — no CQRS opinion. → ListTodosRequest : IRequest<List<Todo>>
[Handler] public Task<List<Todo>> ListTodos() => db.Todos.ToListAsync();
// Query marker. → GetTodoQuery(int Id) : IQuery<Todo?> : IRequest<Todo?>
[Query] public Task<Todo?> GetTodo(int id) => db.Todos.FindAsync(id).AsTask();
// Command marker; the [Authorize] is forwarded onto the generated request.
// → CreateTodoCommand(string Title) : ICommand<int> : IRequest<int>
[Command] [Authorize(Roles = "admin")]
public Task<int> CreateTodo(string title) { /* ... */ }
}
The declaring class is injected from DI (you register it), and its constructor dependencies flow in normally. Dispatch either explicitly or through the generated ergonomic extension:
var todo = await sender.Send(new GetTodoQuery(42)); // explicit
var todo2 = await sender.TodoFeatures().GetTodo(42); // generated grouped ISender proxy
Queries and commands are grouped under their host (sender.TodoFeatures().…) rather than flattened
onto ISender directly, so two features exposing a same-named request (e.g. two GetAlls) never
collide at the call site. The accessor is a classic extension method, so it works down to
netstandard2.0.
ICommand<T>/IQuery<T>are thin markers overIRequest<T>that let you target behaviors (e.g.where TRequest : IQuery<TResponse>). They are additive — hand-written or migrated code that only usesIRequest<T>is unaffected.- Attribute forwarding. Any attribute on the method that is valid on a class (e.g.
[Authorize]) is copied onto the generated request, so authorization and other reflective behaviors work unchanged. - Scope. The generator handles
Task<T>-returning methods; caching is a planned opt-in behavior, not part of this layer.
Service interfaces & pre-shaped requests
The host can also be a service interface (register its implementation with
AddScoped<ITodoService, TodoService>()), and a parameter that already is a request is used
directly instead of being wrapped:
public sealed record CreateTodoCommand(string Title) : ICommand<TodoView>; // your own request DTO
public interface ITodoService // attributes on interface methods
{
[Command] Task<TodoView> CreateAsync(CreateTodoCommand command, CancellationToken ct = default); // pass-through
[Query] Task<TodoView> GetAsync(Guid id, CancellationToken ct = default); // → GetAsyncQuery(Guid Id)
}
- Pass-through —
CreateAsync's parameter already implementsICommand<TodoView>, soCreateTodoCommandis the request; nothing extra is generated.await sender.Send(new CreateTodoCommand("x"))hits the generated handler, which injectsITodoServiceand callsCreateAsync. - Wrapper —
GetAsync's plainGuidbecomesGetAsyncQuery(Guid Id). - The grouped accessor strips the leading
I:await sender.TodoService().CreateAsync(command).
This is the ergonomics of Commandor's service-grouped API, kept in the exact MediatR shape
(IRequestHandler<,>, Send).
None of this touches the drop-in guarantee: the generator only reacts to [Handler]/[Command]/
[Query], so a mechanical MediatR → PediatR migration (which has none of them) compiles
untouched.
Runnable demos:
samples/PediatR.Sample— a console walkthrough of the three modes.samples/PediatR.Sample.Api— a full minimal-API showcase: the Clean Architecture pipeline (logging / unhandled / authorization / validation / performance),[Authorize], FluentValidation, notification fan-out, and streaming — all driven over HTTP.
Performance
On .NET 10, PediatR is faster than MediatR 12.5.0 on Send, pipelines and streams, and on par for
Publish — with equal or lower allocations. From a BenchmarkDotNet MediumRun:
| Scenario | MediatR | PediatR | Ratio | Allocated |
|---|---|---|---|---|
| Send | 81.3 ns | 75.8 ns | 0.93× | 200 B → 200 B |
| Publish (2 handlers) | 165.8 ns | 168.3 ns | 1.02× | 440 B → 440 B |
| Pipeline (3 behaviors) | 253.7 ns | 193.5 ns | 0.76× | 728 B → 632 B |
| Stream (10 items) | 574.9 ns | 288.7 ns | 0.50× | 584 B → 320 B |
Ratio is PediatR ÷ MediatR (lower is better). See BENCHMARKS.md for methodology and how to reproduce.
Pipeline execution order
For a request, behaviors execute outermost → innermost in this order (the first-registered behavior is the outermost):
- Exception action behavior, then exception handler behavior (for the default
ApplyForUnhandledExceptionsstrategy; swapped forApplyForAllExceptions) - Request pre-processors
- Request post-processors
- Your registered
IPipelineBehavior<,>s, in registration order - The request handler (innermost)
Pre-processors therefore run before your behaviors, post-processors run after them, and exception handling wraps everything.
Differences from MediatR
PediatR aims for source-and-behavior compatibility for the mainstream API. A few deliberate notes:
- Single package. All contract types (
IRequest,INotification,Unit,IStreamRequest, …) live in the onePediatRassembly under the same namespaces; there is no separatePediatR.Contractspackage. - Handler lifetime. Discovered handlers are registered using
PediatRServiceConfiguration.Lifetime(defaultTransient). The built-in pre/post/exception behaviors are alwaysTransient. AutoRegisterRequestProcessors. When enabled, auto-discovered pre/post processors are both registered and wired into the pipeline so they run.- Generic handlers. Closed handlers and identity open-generic handlers
(e.g.
Handler<T> : INotificationHandler<T>) are fully supported. The exotic nested-generic closing controlled byRegisterGenericHandlers(defaultfalse) and theMaxGenericType*limits are present on the configuration for API compatibility but not exhaustively generated.
Building & testing
dotnet build PediatR.sln -c Release
dotnet test PediatR.sln -c Release
CI (GitHub Actions) builds both target frameworks and runs the xUnit suite on every push and PR.
License & attribution
PediatR is licensed under the MIT License. It is an independent implementation of the mediator pattern and is not affiliated with, endorsed by, or derived from the source code of MediatR. MediatR is a trademark/project of its respective authors and is licensed separately. Only PediatR's public API contract intentionally matches MediatR's so that migration is mechanical.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
-
net10.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.