Peyk 1.0.0

dotnet add package Peyk --version 1.0.0
                    
NuGet\Install-Package Peyk -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="Peyk" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Peyk" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Peyk" />
                    
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 Peyk --version 1.0.0
                    
#r "nuget: Peyk, 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 Peyk@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=Peyk&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Peyk&version=1.0.0
                    
Install as a Cake Tool

Peyk

Peyk (Ottoman Turkish): a palace courier — a runner who carried the Sultan's messages.

A CQRS-native, source-generator based mediator for .NET.

CI License: MIT

Highlights

  • CQRS first-class. ICommand and IQuery are 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 PEYK001 at 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, IPipelineBehavior follow 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 in Application (references only Peyk.Abstractions), the generator runs in Api and finds them across the project boundary.

Migrating from another mediator

Mechanical for most codebases:

  1. Swap the package and reference Peyk.SourceGenerator.
  2. Change the namespace to Peyk. IRequest<T>, IRequest, IRequestHandler<,>, IRequestHandler<>, INotification, INotificationHandler<>, IPipelineBehavior<,>, IRequestPreProcessor<>, IRequestPostProcessor<,>, Unit, ISender, IPublisher, IMediator keep their names and shapes.
  3. 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> (no TException generic — 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-typed Send/Publish overloads.
  • New for free: turn IRequest<T> into ICommand<T>/IQuery<T> one message at a time to unlock the typed pipelines, and wrap Publish with INotificationPipelineBehavior<T> (no equivalent in classic mediators).

Contributing

PRs welcome — see CONTRIBUTING.md.

License

MIT. No strings, no tiers, no "contact us for pricing".

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Peyk:

Package Downloads
Peyk.Extensions.Diagnostics

OpenTelemetry-ready diagnostics for Peyk: ActivitySource tracing and Metrics for every command, query and notification.

Peyk.Extensions.Transactions

Transactional command pipeline for Peyk: IUnitOfWork abstraction plus a command-only behavior that commits on success and rolls back on failure. ORM-agnostic (EF Core adapter available separately).

Peyk.Extensions.FluentValidation

FluentValidation pipeline behavior for Peyk: validates commands and queries before their handler executes.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 169 7/8/2026