Mediary is a minimal, open-source library for .NET that implements the Request/Handler (Mediator) pattern with pipeline support — inspired by MediatR, but built from scratch with no external dependencies.
Clean request handling, extensible pipeline behaviors, and a DI-friendly architecture — all with zero external dependencies.
Mediary is a lightweight request dispatcher for .NET designed to offer a clean, extensible, and dependency-free alternative to more complex mediators.
It focuses on performance, clarity, and developer control, while maintaining compatibility with the .NET dependency injection ecosystem.
- ⚡ Lightweight and fast — no unnecessary overhead or runtime reflection
- 🧩 Extensible pipeline behaviors — clean middleware-style request handling
- 🧼 Minimalist design — no external dependencies, no magic
- 🧪 Test-friendly — everything is composable and DI-compatible
- 📦 NuGet-ready — simple to install and integrate
- Installation
- Features
- Usage
- Dependency Injection
- Built-in Behaviors
- Handling Commands Without Return Values
- Core Interfaces
- Aliases
- Request Metadata
You can install Mediary via NuGet:
dotnet add package MediaryOr via the NuGet UI in Visual Studio by searching for Mediary.
📦 Available at: https://www.nuget.org/packages/Mediary
- ✅ Async request handling via
IRequest<TResponse> - ✅ Handler support via
IRequestHandler<TResponse, TRequest> - ✅ Semantic result types:
Unit,Success,Created,Updated,Deleted - ✅ Dispatcher support via
IRequestDispatcher - ✅ Middleware support
IRequestPipelineBehavior<TResponse, TRequest> - ✅ Generic and specific pipeline registration
- ✅ Optional
[RequestInfo]metadata for descriptive logging and tooling - ✅ Aliases for semantic intent:
ICommand<TResponse>,IQuery<TResponse>
public class GetAllPlansQuery : IQuery<List<PlanDto>> { }public class GetAllPlansHandler : IRequestHandler<List<PlanDto>, GetAllPlansQuery>
{
public Task<List<PlanDto>> HandleAsync(GetAllPlansQuery request)
{
// Your logic here
}
}public class PlanService
{
private readonly IRequestDispatcher _dispatcher;
public PlanService(IRequestDispatcher dispatcher) =>
_dispatcher = dispatcher;
public Task<List<PlanDto>> GetPlansAsync() =>
_dispatcher.DispatchAsync<GetAllPlansQuery, List<PlanDto>>(new GetAllPlansQuery());
}Mediary supports flexible registration depending on your needs:
services.AddMediary()
.AddRequestHandlersFromAssembly(typeof(Program).Assembly)
.AddPipelineBehaviorsFromAssembly(typeof(Program).Assembly);This will:
- Register the
IRequestDispatcher - Scan and register all
IRequestHandler<,> - Scan and register all
IRequestPipelineBehavior<,>
If you prefer full control but want to use the builder pattern:
services.AddMediary()
.AddRequestHandler<List<PlanDto>, GetAllPlansQuery, GetAllPlansHandler>()
.AddPipelineBehaviors<List<PlanDto>, GetAllPlansQuery, LoggingBehavior<List<PlanDto>, GetAllPlansQuery>>();You can also override the dispatcher:
services.AddMediary<CustomDispatcher>();If you want zero coupling to Mediary’s builder or extension methods, you can register everything manually:
services.AddScoped<IRequestDispatcher, RequestDispatcher>();
services.AddScoped<IRequestHandler<List<PlanDto>, GetAllPlansQuery>, GetAllPlansHandler>();
services.AddScoped<IRequestPipelineBehavior<List<PlanDto>, GetAllPlansQuery>, LoggingBehavior<List<PlanDto>, GetAllPlansQuery>>();This gives you maximum flexibility and full control over dependency injection.
| Interface | Description |
|---|---|
LoggingBehavior<TResponse, TRequest> |
Logs the start and end of a request using ILogger. |
PerformanceBehavior<TResponse, TRequest> |
Measures and logs execution time of each request. |
You can register the logging and performance behaviors globally in two ways:
services.AddMediary()
.AddOpenPipelineBehaviors(typeof(LoggingBehavior<,>))
.AddOpenPipelineBehaviors(typeof(PerformanceBehavior<,>));services.AddScoped(typeof(IRequestPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddScoped(typeof(IRequestPipelineBehavior<,>), typeof(PerformanceBehavior<,>));This will ensure that both behaviors are applied to all requests automatically.
| Interface | Description |
|---|---|
IRequest<TResponse> |
Base request contract used for dispatching |
IRequestHandler<TResponse, TRequest> |
Handles a specific request and returns a response |
IRequestPipelineBehavior<TResponse, TRequest> |
Defines pipeline logic before/after the handler |
IRequestDispatcher |
Responsible for dispatching requests through the pipeline |
Mediary uses a single interface: IRequest<TResponse>.
When you don’t need to return data from a command, you can use semantic result types to indicate intent:
| Type | Purpose |
|---|---|
Unit |
Used when no response is needed (replaces void) |
Success |
Indicates a generic successful result |
Created |
Indicates a resource was created |
Updated |
Indicates a resource was updated |
Deleted |
Indicates a resource was deleted |
These types are lightweight readonly structs defined in Mediary.Core.Results.
public class ClearCacheCommand : IRequest<Unit> { }
public class ClearCacheHandler : IRequestHandler<Unit, ClearCacheCommand>
{
public Task<Unit> HandleAsync(ClearCacheCommand request) =>
Task.FromResult(Unit.Value);
}public class CreateUserCommand : IRequest<Created> { }
public class CreateUserHandler : IRequestHandler<Created, CreateUserCommand>
{
public Task<Created> HandleAsync(CreateUserCommand request) =>
Task.FromResult(Created.Value);
}You can optionally annotate your request types with descriptive metadata using the [RequestInfo] attribute:
[RequestInfo("Creates a new user", "Command", "Users")]
public class CreateUserCommand : ICommand<Guid> { }This helps document the purpose of the request and can be consumed by logging, debugging, or inspection tools.
Both built-in behaviors (LoggingBehavior and PerformanceBehavior) will use this description if available.
This metadata is also accessible from within handlers or behaviors via extension methods.
👉 See RequestInfoAttribute in the source.
For semantic clarity, Mediary exposes aliases for intent-based request types:
| Alias | Inherits | Purpose |
|---|---|---|
IQuery<TResponse> |
IRequest<TResponse> |
Read-only request |
ICommand<TResponse> |
IRequest<TResponse> |
Write command |
Aliases are optional and meant to improve readability:
// Represents: GET /users/{id}
public class GetUserByIdQuery : IQuery<UserDto> { }
// Represents: POST /users
public class CreateUserCommand : ICommand<UserDto> { }MIT License — Free for personal and commercial use.
Open to feedback, ideas, PRs and improvements — feel free to fork and collaborate.
Built with ❤️ by Facundo Juarez — GitHub