Peyk.Extensions.Diagnostics
1.0.0
dotnet add package Peyk.Extensions.Diagnostics --version 1.0.0
NuGet\Install-Package Peyk.Extensions.Diagnostics -Version 1.0.0
<PackageReference Include="Peyk.Extensions.Diagnostics" Version="1.0.0" />
<PackageVersion Include="Peyk.Extensions.Diagnostics" Version="1.0.0" />
<PackageReference Include="Peyk.Extensions.Diagnostics" />
paket add Peyk.Extensions.Diagnostics --version 1.0.0
#r "nuget: Peyk.Extensions.Diagnostics, 1.0.0"
#:package Peyk.Extensions.Diagnostics@1.0.0
#addin nuget:?package=Peyk.Extensions.Diagnostics&version=1.0.0
#tool nuget:?package=Peyk.Extensions.Diagnostics&version=1.0.0
Peyk
Peyk (Ottoman Turkish): a palace courier — a runner who carried the Sultan's messages.
A CQRS-native, source-generator based mediator for .NET.
Highlights
- CQRS first-class.
ICommandandIQueryare separate interfaces with separate, type-safe pipelines: transactions bind to commands only, caching to queries only — enforced at compile time, not by convention. - Zero reflection. A Roslyn incremental generator discovers handlers at compile time — including handlers in referenced assemblies (contracts and handlers in your Application layer,
AddMediator()in your composition root) — and emits concrete DI registrations. No assembly scanning, no runtime type discovery. - NativeAOT-ready, really.
[assembly: PeykBehavior(...)]closes open generic behaviors over every message at compile time, so even value-type responses (Guid,int) flow through pipelines under NativeAOT. CI publishes and runs a native binary on every commit — zero trim/AOT warnings. - Compile-time safety. Forgot a handler?
warning PEYK001at build time, not an exception in production. Two handlers for one command?PEYK002. - Fast. Immutable frozen route table, a zero-overhead dispatch path when a message has no pipeline services, and no reflection anywhere. Sub-60 ns dispatch, ~6 µs from empty container to first response (see Benchmarks).
- Familiar API.
IRequest,IRequestHandler,INotification,IPipelineBehaviorfollow the classic .NET mediator shape — most existing mediator-based codebases migrate mechanically (see Migrating from another mediator). - MIT, forever. This project will never move to a dual-license or source-available model. If sustainability ever becomes a concern, the answer will be GitHub Sponsors — not relicensing.
Packages
| Package | Description |
|---|---|
Peyk |
Core runtime + DI extensions |
Peyk.Abstractions |
Message/handler/pipeline interfaces (netstandard2.0) — reference this from your Application layer |
Peyk.SourceGenerator |
Compile-time handler discovery and registration |
Peyk.Analyzers |
Extra analyzers |
Peyk.Extensions.FluentValidation |
Validation pipeline behavior |
Peyk.Extensions.Transactions |
IUnitOfWork + transactional command behavior |
Peyk.Extensions.Diagnostics |
OpenTelemetry (ActivitySource + Metrics) |
All packages depend only on Peyk.* and Microsoft.* (plus FluentValidation in its optional extension). No third-party framework lock-in.
Quick start
dotnet add package Peyk
dotnet add package Peyk.SourceGenerator
using Peyk;
// Commands mutate state...
public sealed record CreateOrder(string Product) : ICommand<Guid>;
public sealed class CreateOrderHandler : ICommandHandler<CreateOrder, Guid>
{
public Task<Guid> Handle(CreateOrder command, CancellationToken ct) =>
Task.FromResult(Guid.NewGuid());
}
// ...queries read it.
public sealed record GetOrder(Guid Id) : IQuery<OrderDto?>;
public sealed class GetOrderHandler : IQueryHandler<GetOrder, OrderDto?>
{
public Task<OrderDto?> Handle(GetOrder query, CancellationToken ct) => ...;
}
// Program.cs — AddMediator is GENERATED at compile time for this assembly.
builder.Services.AddMediator();
app.MapPost("/orders", (CreateOrder cmd, ISender sender, CancellationToken ct) => sender.Send(cmd, ct));
Message kinds
| Interface | Handler | Semantics |
|---|---|---|
ICommand<T> / ICommand |
ICommandHandler<,> / ICommandHandler<> |
Write; command pipeline (transactions…) |
IQuery<T> |
IQueryHandler<,> |
Read; query pipeline (caching…) |
IRequest<T> / IRequest |
IRequestHandler<,> / IRequestHandler<> |
General purpose |
INotification |
INotificationHandler<> |
0..n handlers; sequential or Task.WhenAll publisher |
IStreamQuery<T> |
IStreamQueryHandler<,> |
IAsyncEnumerable<T> streaming |
Pipeline
Fixed, documented layout — no ordering surprises:
exception handlers
└─ IPipelineBehavior (all requests, registration order)
└─ ICommandPipelineBehavior / IQueryPipelineBehavior (typed)
└─ pre-processors → handler → post-processors
builder.Services.AddMediator(options => options
.AddDiagnosticsBehavior() // OpenTelemetry spans + metrics
.AddLoggingBehavior()
.AddValidationBehavior() // FluentValidation
.AddTransactionalCommands()); // commands only — queries never open a transaction
Notification (event) pipeline
Events get pipelines too — a gap in most mediator libraries. INotificationPipelineBehavior<T>
wraps the whole publish fan-out:
public sealed class OutboxBehavior<TNotification> : INotificationPipelineBehavior<TNotification>
where TNotification : INotification
{
public async Task Handle(TNotification notification, NotificationHandlerDelegate next, CancellationToken ct)
{
await _outbox.CaptureAsync(notification, ct); // runs even when the event has zero handlers
await next(); // skip this call to suppress the event
}
}
INotificationPipelineBehavior (registration order)
└─ INotificationPublisher (sequential / Task.WhenAll strategy)
└─ handlers
Publish-level cross-cutting (outbox, auditing, tracing, filtering) goes in a behavior;
per-handler concerns (retry, error isolation) go in a custom INotificationPublisher.
Behaviors run even for notifications with zero handlers, so outbox capture never misses an event.
NativeAOT
Runtime open-generic DI registration breaks under NativeAOT when a response is a value type. Declare behaviors at compile time instead — the generator closes them over every discovered message:
[assembly: PeykBehavior(typeof(TransactionalCommandBehavior<,>), Order = 1,
Lifetime = PeykBehaviorLifetime.Scoped)]
dotnet publish -p:PublishAot=true → zero warnings. See tests/Peyk.AotTest: a 2.3 MB native binary exercising every message kind.
Benchmarks
.NET 10, x64, BenchmarkDotNet. Numbers below are indicative (ShortRun); the suite in benchmarks/ also measures other popular mediator implementations side by side — run it yourself for a full comparison on your hardware:
dotnet run -c Release --project benchmarks/Peyk.Benchmarks -- --filter "*"
| Scenario | Mean | Allocated |
|---|---|---|
Send (steady state, singleton) |
54.96 ns | 112 B |
Publish (2 handlers) |
86.24 ns | 101 B |
Container build + first Send (cold start) |
5.78 µs | 11.5 KB |
Dispatch overhead is nanoseconds; if a handler touches I/O, the mediator is noise.
Samples
samples/CleanArchitecture— Domain / Application / Infrastructure / Api layering: contracts + handlers live inApplication(references onlyPeyk.Abstractions), the generator runs inApiand finds them across the project boundary.
Migrating from another mediator
Mechanical for most codebases:
- Swap the package and reference
Peyk.SourceGenerator. - Change the namespace to
Peyk.IRequest<T>,IRequest,IRequestHandler<,>,IRequestHandler<>,INotification,INotificationHandler<>,IPipelineBehavior<,>,IRequestPreProcessor<>,IRequestPostProcessor<,>,Unit,ISender,IPublisher,IMediatorkeep their names and shapes. - Replace registration with
services.AddMediator()— no assembly scanning; handlers in referenced assemblies are discovered at compile time.
Differences worth knowing:
- Lifetimes default to
Singleton(mediator and handlers). Handlers depending on scoped services:AddMediator(o => o.Lifetime = ServiceLifetime.Scoped). - Fixed pipeline order:
exception handlers → IPipelineBehavior → typed command/query behaviors → pre → handler → post(registration order within each layer). - Exception handlers are
IRequestExceptionHandler<TRequest, TResponse>(noTExceptiongeneric — pattern-match the exception inside; reflection-on-exception-hierarchy is not AOT-safe). - Streams:
IStreamRequest<T>→IStreamQuery<T>,IStreamRequestHandler<,>→IStreamQueryHandler<,>. - Not in v1: open generic handlers, polymorphic notification dispatch (handle the concrete type), and
object-typedSend/Publishoverloads. - New for free: turn
IRequest<T>intoICommand<T>/IQuery<T>one message at a time to unlock the typed pipelines, and wrapPublishwithINotificationPipelineBehavior<T>(no equivalent in classic mediators).
Contributing
PRs welcome — see CONTRIBUTING.md.
License
MIT. No strings, no tiers, no "contact us for pricing".
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Peyk (>= 1.0.0)
- Peyk.Abstractions (>= 1.0.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Peyk (>= 1.0.0)
- Peyk.Abstractions (>= 1.0.0)
- System.Diagnostics.DiagnosticSource (>= 10.0.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 | 115 | 7/8/2026 |