Peyk.SourceGenerator
1.0.0
dotnet add package Peyk.SourceGenerator --version 1.0.0
NuGet\Install-Package Peyk.SourceGenerator -Version 1.0.0
<PackageReference Include="Peyk.SourceGenerator" Version="1.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="Peyk.SourceGenerator" Version="1.0.0" />
<PackageReference Include="Peyk.SourceGenerator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Peyk.SourceGenerator --version 1.0.0
#r "nuget: Peyk.SourceGenerator, 1.0.0"
#:package Peyk.SourceGenerator@1.0.0
#addin nuget:?package=Peyk.SourceGenerator&version=1.0.0
#tool nuget:?package=Peyk.SourceGenerator&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".
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
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 | 126 | 7/8/2026 |