Mediarq 1.5.1

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

<p align="center"> <img src="assets/logo.svg" alt="Mediarq logo" width="128" height="128"> </p>

Mediarq

CI codecov NuGet License: MIT .NET

A lightweight, dependency-free CQRS mediator for .NET — a free alternative to MediatR with commands, queries, no-result commands, notifications, a composable pipeline of behaviors, and built-in validation and Result types. Designed for domain-driven and CQRS architectures.

  • ✅ Commands / queries returning a Result (railway-oriented)
  • ✅ No-result (void) commands routed through the same pipeline
  • ✅ Notifications published to multiple handlers
  • ✅ Streaming requests (IStreamRequest<T>IAsyncEnumerable<T>)
  • ✅ Pipeline behaviors (logging, performance, validation, exception handling) + your own, orderable
  • ✅ Built-in validation abstraction and Result / ResultError types
  • Reflection-free dispatch via an optional source generator — trimming/Native AOT friendly
  • ✅ Functional Result combinators (Map, Bind, Match, Tap, Ensure)

Targets .NET 8, .NET 9 and .NET 10.


Why Mediarq?

MediatR has moved to a commercial license for many users. Mediarq is a drop-in-shaped, 100% free and MIT-licensed mediator: no license tier, no revenue threshold, no future licensing risk — just a package you dotnet add and keep. It covers the same CQRS building blocks (commands, queries, notifications, a behavior pipeline) plus things MediatR doesn't ship out of the box: built-in Result types, reflection-free/Native AOT dispatch, and a source-generator-driven registration path.

Already on MediatR? The migration guide and its analyzer code-fix provider convert most call sites automatically, and Mediarq.MediatRCompat lets you migrate incrementally in a mixed codebase.

Installation

dotnet add package Mediarq        # lean meta-package: core + lightweight extensions
# or, for the core only:
dotnet add package Mediarq.Core   # mediator, pipeline, results, source generator

The Mediarq meta-package bundles Mediarq.Core with the lightweight extensions (ASP.NET Core, FluentValidation, DataAnnotations, Caching, Diagnostics, UnitOfWork). Heavy or opinionated integrations — Mediarq.EntityFrameworkCore, Mediarq.OpenTelemetry, Mediarq.MassTransit and Mediarq.Polly — ship separately and are installed explicitly when needed (see Extension packages). Reference Mediarq.Core plus only the extensions you need to keep dependencies minimal.

Scaffolding (dotnet new)

Install the templates once, then scaffold either a feature or a full solution:

dotnet new install Mediarq.Templates

# a command, its handler and a validator
dotnet new mediarq-feature -n CreateUser --namespace MyApp.Users

# a runnable ASP.NET Core Web API wired with Mediarq: commands, queries, a FluentValidation
# validator, minimal-API endpoints mapping Result to HTTP, and request logging — all working
# out of the box against an in-memory store
dotnet new mediarq-webapi -n MyApi -o MyApi

Getting started

Register Mediarq, passing the assemblies that contain your handlers, behaviors and validators:

using Mediarq.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLogging();
builder.Services.AddMediarq(isHttp: true, typeof(CreateUserCommand).Assembly);
// `isHttp: true` registers HttpUserContext — remember to also add the HTTP context accessor:
builder.Services.AddHttpContextAccessor();

AddMediarq parameters:

Parameter Description
isHttp When true, registers HttpUserContext (reads the user from HttpContext). Otherwise a DefaultUserContext ("system") is used.
assemblies The assemblies to scan for handlers/behaviors/validators. When omitted, the entry assembly is scanned. The Mediarq assembly itself is always scanned for the built-in behaviors.

Inject IMediator, or the narrower ISender (commands/queries) / IPublisher (notifications).

Reflection-free registration (source generator) — Native AOT

For startup with no assembly scan, use AddMediarqCore together with the compile-time generated AddMediarqHandlers() extension — shipped as an analyzer inside the package:

builder.Services.AddMediarqCore(isHttp: false)
                .AddMediarqHandlers(); // generated at compile time

On this path there is no reflection at all on dispatch: the generator pre-populates a registry of strongly-typed Send/notification wrappers (no Activator.CreateInstance, no MakeGenericType), and the validation pipeline builds Result<T> failures from generated factories. The library is marked IsAotCompatible and publishes cleanly with Native AOT (see Samples/Mediarq.AotSample).

The scan-based AddMediarq(...) is convenient but uses reflection and is annotated [RequiresUnreferencedCode]; prefer AddMediarqCore() + AddMediarqHandlers() for trimming/AOT.

The generated AddMediarqHandlers() is internal and lives in the Mediarq.Extensions namespace by default. Override either via MSBuild:

<PropertyGroup>
  <MediarqGeneratedAccessibility>public</MediarqGeneratedAccessibility>
  <MediarqGeneratedNamespace>MyApp.Generated</MediarqGeneratedNamespace>
</PropertyGroup>

The generator also emits compile-time diagnostics: MQ001 (multiple handlers for one request), MQ002 (a command/query with no handler in the assembly), MQ003 (a validator whose target is neither a request nor a notification, so it can never run), MQ004 (the reflection-based AddMediarq(...) called in a project that publishes with Native AOT), MQ005 (multiple IStreamRequestHandlers for the same stream request), MQ006 (a stream request with no handler in the assembly), and MQ007 (a notification with no handler in the assembly).

Commands & queries (with a result)

public record CreateUserCommand(string Name) : ICommand<Result<Guid>>;

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, Result<Guid>>
{
    public Task<Result<Guid>> Handle(CreateUserCommand request, CancellationToken cancellationToken = default)
    {
        var id = Guid.NewGuid();
        // ... persist ...
        return Task.FromResult(Result.Success(id));
    }
}

// Dispatch
Result<Guid> result = await mediator.Send(new CreateUserCommand("Alice"));
if (result.IsSuccess) { /* use result.Value */ }

Queries work the same way via IQuery<TResponse> / IQueryHandler<TQuery, TResponse>.

No-result (void) commands

A command without a return value implements ICommand and is handled by ICommandHandler<TCommand>. It flows through the same pipeline as any other request (its response type is Unit).

public record DeleteUserCommand(Guid Id) : ICommand;

public class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
{
    public Task Handle(DeleteUserCommand request, CancellationToken cancellationToken = default)
    {
        // ... delete ...
        return Task.CompletedTask;
    }
}

await mediator.Send(new DeleteUserCommand(id));

Notifications

A notification can be handled by zero or more handlers. All handlers are invoked when published.

public record UserCreated(Guid Id) : INotification;

public class SendWelcomeEmail : INotificationHandler<UserCreated>
{
    public Task Handle(UserCreated notification, CancellationToken cancellationToken = default)
        => /* ... */ Task.CompletedTask;
}

await mediator.Publish(new UserCreated(id));

By default handlers run concurrently (ParallelNotificationPublisher) and the first failure is surfaced; publishing with no registered handler is a no-op. Register a different INotificationPublisher (e.g. SequentialNotificationPublisher, or your own) before AddMediarq/AddMediarqCore to change this.

Polymorphic notifications (opt-in)

By default, publishing resolves handlers for the notification's exact concrete type only — the reflection-free fast path. Implement IPolymorphicNotification to also dispatch to INotificationHandler<TBase> for every base type in the notification's class hierarchy:

public abstract record DomainEvent : INotification;
public sealed record OrderPlaced(Guid OrderId) : DomainEvent, IPolymorphicNotification;

// Receives OrderPlaced (and any other DomainEvent-derived type), not just its own concrete type:
public class AuditLogHandler : INotificationHandler<DomainEvent> { /* ... */ }

Concrete-type handlers run first, then base-type handlers from most to least specific — unless a handler implements IOrderedNotificationHandler, in which case its explicit Order takes precedence across the whole batch. This is opt-in and per-notification-type: publishing a type that doesn't implement IPolymorphicNotification is unaffected — and, unlike the default path, it does resolve handlers via reflection (walking the base-type hierarchy with MakeGenericType), so it's not part of the trimming/AOT fast path.

Cascaded notifications — Result.WithNotifications(...) (opt-in)

A handler can attach follow-up notifications to its own result instead of injecting IPublisher and calling Publish(...) itself — what the handler causes to happen next shows up in its return value:

public Task<Result<Guid>> Handle(CreateOrder request, CancellationToken cancellationToken = default)
{
    var id = Guid.NewGuid();
    // ... persist the order ...
    return Task.FromResult(Result.Success(id).WithNotifications(new OrderPlaced(id)));
}

The mediator publishes attached notifications automatically once the request finishes dispatching — after every behavior/exception handler/post-processor has run, and only when the result is a success — through the same IPublisher/INotificationPublisher as an explicit Publish(...) call. See Wiring extensions.

Out-of-process notifications (MassTransit)

The optional Mediarq.MassTransit package forwards notifications to a MassTransit bus, so other services can consume them out-of-process. The forwarder is a regular notification handler, so it runs alongside your in-process handlers.

// Configure MassTransit as usual (provides IPublishEndpoint), then:
builder.Services.AddMediarqMassTransitForwarding<OrderPlaced>();          // one event, or
builder.Services.AddMediarqMassTransitForwarding(typeof(OrderPlaced).Assembly); // every IIntegrationEvent

public record OrderPlaced(Guid Id) : IIntegrationEvent; // IIntegrationEvent : INotification

await mediator.Publish(new OrderPlaced(id)) now runs the local handlers and publishes the event on the bus.

Pipeline behaviors

Cross-cutting logic wraps the handler. The pipeline is lean by default: the built-in ValidationBehavior, pre/post-processor and exception behaviors register only when you actually have a validator / processor / exception handler, so an idle request resolves no behavior at all. Request logging and performance tracking are opt-in:

services.AddMediarq(/* ... */)
        .AddMediarqRequestLogging()       // LoggingBehavior
        .AddMediarqPerformanceTracking()  // PerformanceBehavior
        .AddMediarqTimeout();             // TimeoutBehavior

AddMediarqTimeout() bounds requests that implement ITimeoutRequest: if handling exceeds the request's Timeout, a RequestTimeoutException is thrown (a pessimistic timeout — it frees the caller, so handlers should also honor their CancellationToken). It is inert for other request types.

Add your own by implementing IPipelineBehavior<TRequest, TResponse>:

public class AuditBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : ICommandOrQuery<TResponse>
{
    public async Task<TResponse> Handle(
        IMutableRequestContext<TRequest, TResponse> context,
        Func<Task<TResponse>> handle,
        CancellationToken cancellationToken = default)
    {
        // before
        var response = await handle();
        // after
        return response;
    }
}

Behaviors discovered by the scan run in registration order. To control ordering, also implement IOrderBehavior — behaviors with a lower Order run first (outermost):

public class AuditBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>, IOrderBehavior
    where TRequest : ICommandOrQuery<TResponse>
{
    public int Order => 10;
    // ...
}

A behavior can also opt out of the pipeline per request type by implementing IConditionalPipelineBehavior and returning IsActive => false — the executor then omits it entirely, adding neither an async frame nor a delegate. The built-in behaviors use this so an idle pipeline costs nothing: validation/pre/post/exception activate only when a validator, processor or exception handler is registered, and logging/performance only when the matching log level is enabled. As a result, dispatch for a request with no active behavior goes straight to the handler — allocations are on par with MediatR.

Validation

Implement IValidator<TRequest>; the ValidationBehavior runs all validators before the handler and short-circuits with a failed Result / Result<T> (carrying a ValidationError) when invalid:

public class CreateUserCommandValidator : IValidator<CreateUserCommand>
{
    public IEnumerable<ValidationResult> Validate(CreateUserCommand instance)
    {
        if (string.IsNullOrWhiteSpace(instance.Name))
            yield return ValidationResult.Failure([new ValidationPropertyError(nameof(instance.Name), "Name is required.")]);
        else
            yield return ValidationResult.Success();
    }
}

Notifications are validated too: define an IValidator<TNotification> and it runs automatically before the notification is published. Because a notification has no return value, an invalid one throws a NotificationValidationException (carrying the property errors) instead of returning a failed Result.

The Result type

Result / Result<T> express success or failure without exceptions:

Result ok = Result.Success();
Result<int> value = Result.Success(42);
Result failed = Result.Failure(ResultError.NotFound("User.NotFound", "User not found"));

if (value.IsSuccess) Console.WriteLine(value.Value);

Compose them functionally, without manual IsSuccess checks (sync + async variants):

string message =
    Result.Success(42)
        .Ensure(x => x > 0, ResultError.Failure("Id.Invalid", "must be positive"))
        .Map(x => x * 2)
        .Match(onSuccess: x => $"value: {x}", onFailure: e => $"error: {e.Message}");

// async, over Task<Result<T>>
Result<int> doubled = await GetResultAsync().MapAsync(x => x * 2);

Extension packages

Mediarq ships optional, opt-in packages so the core stays dependency-free:

Package Purpose
Mediarq.AspNetCore Map Result / ResultErrorIResult and RFC 7807 ProblemDetails
Mediarq.FluentValidation Run FluentValidation validators in the Mediarq pipeline
Mediarq.DataAnnotations Validate requests with System.ComponentModel.DataAnnotations attributes (AddMediarqDataAnnotations)
Mediarq.Caching Memoize responses of ICacheableRequest via IMemoryCache (AddMediarqCaching) or IDistributedCache / Redis (AddMediarqDistributedCaching)
Mediarq.Idempotency Run IIdempotentRequest at most once per key, replaying the stored result (AddMediarqIdempotency)
Mediarq.Idempotency.EntityFrameworkCore EF Core-backed IDistributedCache for Mediarq.Idempotency, no Redis required (AddMediarqIdempotencyEntityFrameworkCore)
Mediarq.Outbox Transactional outbox over EF Core: enqueue notifications and publish them reliably (AddMediarqOutbox)
Mediarq.Saga Saga / process-manager primitives: persisted, correlated state across a sequence of notifications (AddMediarqSaga<TState>)
Mediarq.Diagnostics Activity tracing + metrics (OpenTelemetry-compatible) (AddMediarqDiagnostics)
Mediarq.OpenTelemetry One-line AddMediarqInstrumentation() on the tracer/meter provider builders
Mediarq.UnitOfWork Commit a unit of work around ITransactionalRequest commands (AddMediarqUnitOfWork)
Mediarq.EntityFrameworkCore EfCoreUnitOfWork<TContext> over a DbContext (AddMediarqEntityFrameworkCore)
Mediarq.Polly Retry / timeout / circuit breaker for IResilientRequest via Polly (AddMediarqResilience)
Mediarq.MassTransit Forward notifications to a MassTransit bus, out-of-process (AddMediarqMassTransitForwarding)
Mediarq.MediatRCompat Optional MediatR compatibility shim for incremental migration (AddMediarqMediatRCompat)
Mediarq.Hangfire Enqueue/schedule a command as a Hangfire background job, dispatched through the real pipeline (AddMediarqHangfire)
Mediarq.Quartz Enqueue/schedule a command as a Quartz.NET job, dispatched through the real pipeline (AddMediarqQuartz)
Mediarq.HealthChecks IHealthCheck + startup validation that every command/query resolves to exactly one handler (AddMediarqHandlerRegistrationCheck, AddMediarqHandlerValidationOnStartup)
Mediarq.Authorization ASP.NET Core policy-based authorization as a pipeline behavior for IAuthorizedRequest (AddMediarqAuthorization)
Mediarq.Testing SpyMediator decorator recording dispatched requests/notifications through the real pipeline, plus FakeClock/FakeUserContext (AddMediarqSpy)
Mediarq.RateLimiting Throttle IRateLimitedRequest requests via System.Threading.RateLimiting, partitionable per user/key (AddMediarqRateLimiting)
Mediarq.Deferred In-process deferred dispatch on a System.Threading.Channels background worker, no external dependency (AddMediarqDeferredDispatch)
Mediarq.Dapr Dapr pub/sub: publish IDaprPubSubEvent notifications (AddMediarqDaprPubSub) and receive them back into the pipeline via a minimal-API webhook + /dapr/subscribe (MapDaprPubSubSubscription)
Mediarq.AzureServiceBus A lightweight, direct Azure Service Bus bridge: publish IAzureServiceBusEvent notifications (AddMediarqAzureServiceBusPublisher) and consume them back into the pipeline via a background service (AddMediarqAzureServiceBusSubscriber)
Mediarq.RabbitMQ A lightweight, direct RabbitMQ bridge: publish IRabbitMqEvent notifications (AddMediarqRabbitMqPublisher) and consume them back into the pipeline via a background service (AddMediarqRabbitMqSubscriber)
Mediarq.Aspire .NET Aspire ServiceDefaults integration: wires Mediarq's OpenTelemetry instrumentation and handler-registration health check into your own ServiceDefaults project (AddMediarqServiceDefaults)
Mediarq.Grpc Direct point-to-point gRPC transport: publish IGrpcNotificationEvent notifications to another service's endpoint (AddMediarqGrpcPublisher) and receive them back into the pipeline via a shared gRPC service (AddMediarqGrpcSubscriptions + MapMediarqGrpcSubscription) — ships its own compiled Protobuf contract, no protoc needed downstream

Built into Mediarq.Core:

  • Exception handling — implement IRequestExceptionHandler<TRequest, TResponse> to turn an exception into a response (typically a failed Result).
  • Pre/post processorsIRequestPreProcessor<TRequest> and IRequestPostProcessor<TRequest, TResponse> run around the handler.
  • Streaming pipelineIStreamPipelineBehavior<TRequest, TResponse> wraps CreateStream with the same ordering as Send behaviors.
  • Ordered notifications — a notification handler can implement IOrderedNotificationHandler for a deterministic order.
  • Lifetime control — opt a handler into a DI lifetime with [RegisterHandler(ServiceLifetime.Singleton)].
  • Validation localization — translate messages via IValidationMessageResolver.
  • More Result combinatorsCombine, Try/TryAsync, TryGetValue, Recover, OrElse, ToResult, plus cross async MapAsync/BindAsync overloads.
  • AggregateExceptionNotificationPublisher — runs every notification handler and surfaces all failures.

Samples

Three runnable samples under Samples/ (see Samples/README.md):

  • Mediarq.Samples.Quickstart — a console tour of the core in-process features (commands/queries/void, notifications, streaming, validation, behaviors, pre/post processors, exception handling, timeout, Result combinators).
  • Mediarq.Samples.WebApi — an ASP.NET Core "Orders" API wiring the extensions end-to-end (Result → HTTP, FluentValidation/DataAnnotations, caching, idempotency, EF Core unit of work + transactional outbox + domain events + cascaded notifications, policy-based authorization, rate limiting, health checks, Polly, diagnostics/OpenTelemetry, MassTransit).
  • Mediarq.AotSample — the reflection-free path, published with Native AOT.
dotnet run --project Samples/Mediarq.Samples.Quickstart
dotnet run --project Samples/Mediarq.Samples.WebApi      # then open /scalar/v1
dotnet run --project Samples/Mediarq.AotSample

The Quickstart sample above is what assets/quickstart.tape records into a GIF via VHS (vhs assets/quickstart.tape) — regenerate it whenever the sample's console output changes.

Documentation

Task-focused guides live under docs/guides (and on the docs site). If you're starting from scratch with no one to ask, read them in this order:

  1. Concepts — commands vs queries vs notifications, Result, the pipeline.
  2. Your first app — build a working API step by step.
  3. Wiring extensions — register the core and each optional package (with the prerequisites and gotchas).
  4. Writing a behavior · Testing · Migrating from MediatR · Native AOT & trimming.
  5. Troubleshooting — when something silently doesn't fire (start here when stuck).

License

MIT © Nicolas Rouffart

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

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.5.1 275 7/26/2026
1.5.0 177 7/26/2026
1.4.0 251 7/24/2026
1.3.0 268 7/24/2026
1.2.0 180 7/24/2026
1.1.0 171 7/24/2026
1.0.0 311 6/27/2026