Skip to main content

Getting Started

ZeroAlloc.Mediator is a zero-allocation, compile-time-dispatched mediator library for .NET 8 and .NET 10 that implements the mediator pattern without runtime reflection. Rather than using a dictionary lookup at runtime, it leverages a Roslyn source generator to emit strongly-typed dispatch methods at build time, making it fully Native AOT-compatible. Benchmarks show the static Mediator class is 3.9–20x faster than MediatR on Send and Publish, with zero heap allocations on those paths; see Performance.

Installation​

dotnet add package ZeroAlloc.Mediator
dotnet add package ZeroAlloc.Mediator.Generator

ZeroAlloc.Mediator.Generator is the source generator. It is a development dependency, so dotnet add package writes it with PrivateAssets="all". If you add it by hand, keep that attribute:

<PackageReference Include="ZeroAlloc.Mediator.Generator" Version="*" OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />

PrivateAssets="all" matters in multi-project solutions: without it the analyzer flows transitively across a ProjectReference, so a downstream project runs the generator a second time and gets CS0436 on the duplicate generated types, a hard failure under TreatWarningsAsErrors.

Nothing else needs configuring. The generator runs as part of your normal dotnet build and emits all dispatch code into your project.

Your First Request​

The following example walks through placing an order to illustrate the full request/handler/send cycle.

Step 1 — Define the request​

Requests must be readonly record struct types (not classes). The type implements IRequest<TResponse> where TResponse is the value returned by the handler.

using ZeroAlloc.Mediator;

public readonly record struct PlaceOrderCommand(
string CustomerId,
IReadOnlyList<OrderLineItem> Items
) : IRequest<OrderId>;

public readonly record struct OrderId(Guid Value);
public readonly record struct OrderLineItem(string Sku, int Quantity, decimal UnitPrice);

Step 2 — Implement the handler​

Create a class that implements IRequestHandler<TRequest, TResponse>. The handler receives the request and a CancellationToken, and returns a ValueTask<TResponse>.

public class PlaceOrderHandler : IRequestHandler<PlaceOrderCommand, OrderId>
{
public ValueTask<OrderId> Handle(PlaceOrderCommand command, CancellationToken ct)
{
// In a real app, persist the order here
var id = new OrderId(Guid.NewGuid());
return ValueTask.FromResult(id);
}
}

Step 3 — Send the request​

Call the static Mediator.Send method. The overload is generated specifically for PlaceOrderCommand, so the call is fully typed — no casting, no boxing.

var command = new PlaceOrderCommand("customer-42", [
new OrderLineItem("SKU-001", 2, 29.99m)
]);

OrderId orderId = await Mediator.Send(command);
Console.WriteLine($"Order placed: {orderId.Value}");

Step 4 — Build​

Just run dotnet build. The source generator emits the Mediator.Send overload for PlaceOrderCommand automatically — nothing else to configure.

What Gets Generated​

After building, the source generator emits a strongly-typed overload for every request type it discovers. For PlaceOrderCommand the output looks roughly like this (illustration only):

// Auto-generated by ZeroAlloc.Mediator.Generator
public static partial class Mediator
{
public static ValueTask<OrderId> Send(
PlaceOrderCommand request, CancellationToken ct = default)
=> (_placeOrderHandlerFactory?.Invoke() ?? new PlaceOrderHandler())
.Handle(request, ct);
}

One strongly-typed overload is generated per request type. There is no dictionary lookup and no reflection involved at any point in the dispatch path.

Architecture Overview​

The sequence below shows how a Send call flows from your application code through the generated mediator to the handler and back.

Key Concepts​

  • Requests: Implement IRequest<TResponse> for request/response interactions, or IRequest for fire-and-forget operations that return Unit.
  • Notifications: Implement INotification to publish events to multiple handlers. Dispatch can be sequential, parallel, or polymorphic depending on configuration.
  • Streaming: Implement IStreamRequest<T> to receive an IAsyncEnumerable<T> result, suited for large or paginated result sets.
  • Pipeline Behaviors: Apply cross-cutting concerns such as logging, validation, or caching with a class that implements IPipelineBehavior, carries the [PipelineBehavior] attribute and has a static Handle method. The class itself must not be static, or the generator skips it.
  • Zero allocation: Always declare request types as readonly record struct. Using a class triggers compiler diagnostic ZAM003 and forfeits the zero-allocation guarantee.

Next Steps​

GuideWhat you'll learn
Requests & HandlersCommands, queries, Unit responses
NotificationsEvents: sequential, parallel, polymorphic
StreamingIAsyncEnumerable for large result sets
Pipeline BehaviorsLogging, validation, caching middleware
Dependency InjectionDI containers, IMediator, factories
DiagnosticsZAM001–ZAM011 compiler error reference
PerformanceZero-alloc internals, benchmarks, AOT
Advanced PatternsError handling, cancellation, scoped behaviors
TestingUnit-test handlers, behaviors, and notifications