Zibetti.Mediator
1.1.0
dotnet add package Zibetti.Mediator --version 1.1.0
NuGet\Install-Package Zibetti.Mediator -Version 1.1.0
<PackageReference Include="Zibetti.Mediator" Version="1.1.0" />
<PackageVersion Include="Zibetti.Mediator" Version="1.1.0" />
<PackageReference Include="Zibetti.Mediator" />
paket add Zibetti.Mediator --version 1.1.0
#r "nuget: Zibetti.Mediator, 1.1.0"
#:package Zibetti.Mediator@1.1.0
#addin nuget:?package=Zibetti.Mediator&version=1.1.0
#tool nuget:?package=Zibetti.Mediator&version=1.1.0
Zibetti.Mediator
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: throwsInvalidOperationExceptionif no handler or multiple handlers are registered.PublishAsync: all handlers run even if some throw; exceptions are collected and re-thrown asAggregateException.
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
IMediatoras scoped and the open-genericIChain<,>as transient - Scans all provided assemblies for
ICommandHandler<>,ICommandHandler<,>,IQueryHandler<,>,IEventHandler<>, andIChainHandler<,>implementations and registers them as scoped
AddPipelineBehavior<TBehavior>():
- Registers a behavior as scoped
- Applied in registration order (first = outermost wrapper)
Contributing
- Fork the repo and create a feature branch
- Write tests first (xUnit + FluentAssertions)
- Run
dotnet test— all tests must pass - Run
dotnet stryker— mutation score must meet the threshold (≥ 85%) - 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 | Versions 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. |
-
net10.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.