Zibetti.Mediator 1.1.0

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

Zibetti.Mediator

Mutation Score

A minimal, DI-first .NET 10 mediator library. No magic, no global state — just clean request/handler contracts wired through the standard IServiceCollection.

Why Zibetti.Mediator?

Libraries like MediatR and Brighter are capable but carry significant overhead: custom registries, opaque decorator chains, and Unit sentinels. Zibetti.Mediator is built around one principle: the DI container is your handler registry. Register a handler, dispatch a request — that's it.

Feature Zibetti.Mediator MediatR Brighter
DI-native resolution ✓ Partial ✗ (SubscriberRegistry)
Distinct command/query/event types ✓ ✗ (all IRequest<T>) ✓
Pipeline behaviors ✓ ✓ ✓ (decorators)
Zero extra dependencies ✓ ✓ ✗
Targets .NET 10 ✓ ✓ ✓

Installation

dotnet add package Zibetti.Mediator

Concepts

Request Marker Interfaces

Requests are plain types that implement one of four markers. The marker determines the dispatch semantics.

// Fire-and-forget — no result
public record DeleteUserCommand(Guid UserId) : ICommand;

// Command that returns a value
public record CreateUserCommand(string Email) : ICommand<Guid>;

// Read-only query
public record GetUserQuery(Guid UserId) : IQuery<UserDto>;

// Notification dispatched to all handlers (fan-out)
public record UserDeletedEvent(Guid UserId) : IEvent;

Handler Interfaces

Each marker interface has a corresponding handler interface. Register exactly one handler per command or query; events support multiple handlers.

ICommandHandler<TCommand> — fire-and-forget
public class DeleteUserHandler(IUserRepository repo) : ICommandHandler<DeleteUserCommand>
{
    public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken = default)
    {
        await repo.DeleteAsync(command.UserId, cancellationToken);
    }
}
ICommandHandler<TCommand, TResult> — command with result
public class CreateUserHandler(IUserRepository repo) : ICommandHandler<CreateUserCommand, Guid>
{
    public async Task<Guid> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
    {
        var user = new User(command.Email);
        await repo.AddAsync(user, cancellationToken);
        return user.Id;
    }
}
IQueryHandler<TQuery, TResult> — read-only query
public class GetUserHandler(IUserRepository repo) : IQueryHandler<GetUserQuery, UserDto>
{
    public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken = default)
    {
        var user = await repo.FindAsync(query.UserId, cancellationToken);
        return new UserDto(user.Id, user.Email);
    }
}
IEventHandler<TEvent> — event notification (fan-out)
public class AuditOnUserDeleted : IEventHandler<UserDeletedEvent>
{
    public Task HandleAsync(UserDeletedEvent evt, CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Audit: user {evt.UserId} deleted");
        return Task.CompletedTask;
    }
}

public class CleanupOnUserDeleted(IFileService files) : IEventHandler<UserDeletedEvent>
{
    public async Task HandleAsync(UserDeletedEvent evt, CancellationToken cancellationToken = default)
    {
        await files.DeleteUserFilesAsync(evt.UserId, cancellationToken);
    }
}

Pipeline Behaviors

IPipelineBehavior<TRequest, TResponse> is middleware-style cross-cutting logic that wraps handler dispatch. Behaviors execute in registration order (first registered = outermost wrapper).

Validation behavior example
public class ValidationBehavior<TRequest, TResponse>(IValidator<TRequest> validator)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    public async Task<TResponse> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken = default)
    {
        await validator.ValidateAndThrowAsync(request, cancellationToken);
        return await next(cancellationToken);
    }
}

Behaviors can also short-circuit (not call next) or modify the result returned by the handler.


Chain of Responsibility

IChainHandler<TRequest, TResult> + IChain<TRequest, TResult> provide strategy selection: a request is dispatched to the first registered link — ordered by ascending Order — whose CanHandle returns true. This is distinct from pipeline behaviors (which always wrap every dispatch); a chain picks one handler among many.

public record NotificationContent(string PackageName, string Text);

// One link per source — add a new source by adding a new link (OCP, no edits elsewhere).
public class IFoodParser : IChainHandler<NotificationContent, ParsedTransaction?>
{
    public bool CanHandle(NotificationContent c) => c.PackageName == "com.ifood.benefits";
    public Task<ParsedTransaction?> HandleAsync(NotificationContent c, CancellationToken ct = default)
        => Task.FromResult(Parse(c.Text));
}

// Fallback: catches anything unmatched so the chain never throws.
public class UnknownFallback : IChainHandler<NotificationContent, ParsedTransaction?>
{
    public int Order => int.MaxValue;
    public bool CanHandle(NotificationContent c) => true;
    public Task<ParsedTransaction?> HandleAsync(NotificationContent c, CancellationToken ct = default)
        => Task.FromResult<ParsedTransaction?>(null);
}
// Inject the chain and execute:
public class IngestHandler(IChain<NotificationContent, ParsedTransaction?> chain)
{
    public Task<ParsedTransaction?> RunAsync(NotificationContent c, CancellationToken ct)
        => chain.ExecuteAsync(c, ct);
}

Error semantics: ExecuteAsync throws InvalidOperationException when no registered link can handle the request — register a fallback link (CanHandle => true, Order = int.MaxValue) when "no match" should be a no-op instead.


Dispatch Methods

All dispatch is through IMediator:

// Fire-and-forget command
await mediator.SendAsync(new DeleteUserCommand(userId), ct);

// Command with result
var newId = await mediator.SendAsync<CreateUserCommand, Guid>(new CreateUserCommand(email), ct);

// Query
var user = await mediator.QueryAsync<GetUserQuery, UserDto>(new GetUserQuery(userId), ct);

// Event — all handlers run; exceptions are aggregated
await mediator.PublishAsync(new UserDeletedEvent(userId), ct);

Error semantics:

  • SendAsync / QueryAsync: throws InvalidOperationException if no handler or multiple handlers are registered.
  • PublishAsync: all handlers run even if some throw; exceptions are collected and re-thrown as AggregateException.

DI Registration

Minimal Program.cs setup:

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddZibettiMediator(typeof(Program).Assembly)           // scan for handlers
    .AddPipelineBehavior<ValidationBehavior<CreateUserCommand, Guid>>() // explicit behaviors
    .AddPipelineBehavior<LoggingBehavior<CreateUserCommand, Guid>>();   // in registration order

var app = builder.Build();

AddZibettiMediator(params Assembly[]):

  • Registers IMediator as scoped and the open-generic IChain<,> as transient
  • Scans all provided assemblies for ICommandHandler<>, ICommandHandler<,>, IQueryHandler<,>, IEventHandler<>, and IChainHandler<,> implementations and registers them as scoped

AddPipelineBehavior<TBehavior>():

  • Registers a behavior as scoped
  • Applied in registration order (first = outermost wrapper)

Contributing

  1. Fork the repo and create a feature branch
  2. Write tests first (xUnit + FluentAssertions)
  3. Run dotnet test — all tests must pass
  4. Run dotnet stryker — mutation score must meet the threshold (≥ 85%)
  5. Open a pull request — CI will run build, tests, and Stryker with results posted as a PR comment

License

This project is licensed under the GNU General Public License v3.0 (GPL-3.0). You are free to use, modify, and distribute this software under the terms of the GPL-3.0. See the LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET 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.1.0 152 6/22/2026
1.0.1 196 5/29/2026