Requestum 1.1.6
See the version list below for details.
dotnet add package Requestum --version 1.1.6
NuGet\Install-Package Requestum -Version 1.1.6
<PackageReference Include="Requestum" Version="1.1.6" />
<PackageVersion Include="Requestum" Version="1.1.6" />
<PackageReference Include="Requestum" />
paket add Requestum --version 1.1.6
#r "nuget: Requestum, 1.1.6"
#:package Requestum@1.1.6
#addin nuget:?package=Requestum&version=1.1.6
#tool nuget:?package=Requestum&version=1.1.6
Requestum
Requestum is a lightweight and explicit CQRS library for .NET that provides a structured and strongly-typed approach to handling commands, queries, event messages, and middleware pipelines.
✨ Key Features
- 🎯 Clear separation of Commands, Queries, and Events (CQRS + Event-driven architecture)
- 💪 Strongly-typed handlers for sync and async execution
- 📢 Event publishing with multiple receivers support
- 🔌 Middleware pipeline support for cross-cutting concerns
- 🏗️ Dependency Injection integration with
Microsoft.Extensions.DependencyInjection - ⚡ High performance with minimal overhead
- 🔍 Assembly scanning for automatic registration
- 📦 Lightweight - minimal dependencies
- 🔄 Compatible with .NET 8 and .NET 9
📦 Installation
dotnet add package Requestum
🤔 Why Requestum?
Explicit CQRS separation:
Requestum provides dedicated interfaces for commands, queries, and events, making your intent crystal clear at a glance:
// Commands that modify state
public record CreateUserCommand : ICommand;
public record DeleteUserCommand(int UserId) : ICommand;
// Queries that retrieve data
public record GetUserQuery(int Id) : IQuery<UserDto>;
public record SearchUsersQuery(string Term) : IQuery<List<UserDto>>;
// Events that notify about state changes
public record UserCreatedEvent(int UserId, string Name) : IEventMessage;
public record UserDeletedEvent(int UserId) : IEventMessage;
Clear and descriptive method names:
Each handler type has purpose-built methods that clearly express what they do:
// Command handlers execute actions
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct = default)
{
// Clearly executing a command
}
}
// Query handlers handle requests and return data
public class GetUserHandler : IAsyncQueryHandler<GetUserQuery, UserDto>
{
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken ct = default)
{
// Clearly handling a query and returning data
}
}
// Event receivers receive notifications
public class UserCreatedEmailNotifier : IAsyncEventMessageReceiver<UserCreatedEvent>
{
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
{
// Clearly receiving and processing an event
}
}
True synchronous support:
Requestum provides genuine synchronous handlers and middleware when you don't need async - resulting in better performance and cleaner code:
// Synchronous command handler for operations without I/O
public class ValidateUserHandler : ICommandHandler<ValidateUserCommand>
{
public void Execute(ValidateUserCommand command)
{
// Pure synchronous code - faster and more efficient
if (string.IsNullOrEmpty(command.Name))
throw new ValidationException("Name is required");
}
}
// Synchronous middleware for non-async operations
public class ValidationMiddleware<TRequest, TResponse> : IRequestMiddleware<TRequest, TResponse>
{
public TResponse Invoke(TRequest request, RequestNextDelegate<TRequest, TResponse> next)
{
// Synchronous validation - no async overhead
ValidateRequest(request);
return next.Invoke(request);
}
}
// Asynchronous handlers for I/O operations
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct = default)
{
await _database.SaveAsync(command, ct);
}
}
Performance benefits:
- 🚀 20-50% faster execution with synchronous handlers
- 💾 30-60% less memory allocation
- ⚡ No async state machine overhead for simple operations
- 🎯 Choose sync or async based on your actual needs
Intuitive execution API:
Requestum provides dedicated methods for commands, queries, and events, making your code self-documenting:
// Commands execute
await _requestum.ExecuteAsync(new CreateUserCommand());
_requestum.Execute(new ValidateUserCommand());
// Queries handle and return results
var user = await _requestum.HandleAsync<GetUserQuery, UserDto>(new GetUserQuery(1));
var result = _requestum.Handle<CalculateQuery, ResultDto>(query);
// Events publish to all registered receivers
await _requestum.PublishAsync(new UserCreatedEvent(userId, userName));
_requestum.Publish(new OrderProcessedEvent(orderId));
📋 Usage Examples
Commands
Commands represent operations that change system state and don't return values.
// Synchronous command
public record CreateUserCommand : ICommand
{
public string Name { get; set; } = "";
public string Email { get; set; } = "";
}
public class CreateUserHandler : ICommandHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
public CreateUserHandler(IUserRepository repository)
{
_repository = repository;
}
public void Execute(CreateUserCommand command)
{
var user = new User(command.Name, command.Email);
_repository.Create(user);
}
}
// Execute synchronously
requestum.Execute(new CreateUserCommand { Name = "Alice", Email = "alice@example.com" });
// Or execute asynchronously
await requestum.ExecuteAsync(new CreateUserCommand { Name = "Bob", Email = "bob@example.com" });
Queries
Queries retrieve data without modifying system state and return typed responses.
// Define query and response
public record SumQuery : IQuery<SumQueryResponse>
{
public int A { get; set; }
public int B { get; set; }
}
public record SumQueryResponse
{
public int C { get; set; }
}
// Define handler
public class SumQueryHandler : IQueryHandler<SumQuery, SumQueryResponse>
{
public SumQueryResponse Handle(SumQuery query)
{
if (query.B == 0) throw new Exception("B cannot be 0");
return new SumQueryResponse { C = query.A + query.B };
}
}
// Execute query
var sumQuery = new SumQuery { A = 40, B = 2 };
var result = requestum.Handle<SumQuery, SumQueryResponse>(sumQuery);
Console.WriteLine($"Result: {result.C}"); // Output: Result: 42
// Or asynchronously
var asyncResult = await requestum.HandleAsync<SumQuery, SumQueryResponse>(new SumQuery { A = 10, B = 5 });
Events
Events represent notifications about something that has happened in the system. Unlike commands and queries, events can have multiple receivers (0 to N), enabling publish/subscribe patterns.
// Define event message
public record UserCreatedEvent : IEventMessage
{
public int UserId { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
}
// Define receivers - you can have multiple receivers for the same event
public class SendWelcomeEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
private readonly IEmailService _emailService;
public SendWelcomeEmailReceiver(IEmailService emailService)
{
_emailService = emailService;
}
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken cancellationToken = default)
{
await _emailService.SendWelcomeEmailAsync(message.Email, message.Name, cancellationToken);
}
}
public class LogUserCreationReceiver : IEventMessageReceiver<UserCreatedEvent>
{
private readonly ILogger _logger;
public LogUserCreationReceiver(ILogger logger)
{
_logger = logger;
}
public void Receive(UserCreatedEvent message)
{
_logger.LogInformation($"User created: {message.UserId} - {message.Name}");
}
}
public class UpdateAnalyticsReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
private readonly IAnalyticsService _analytics;
public UpdateAnalyticsReceiver(IAnalyticsService analytics)
{
_analytics = analytics;
}
public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken cancellationToken = default)
{
await _analytics.TrackUserRegistrationAsync(message.UserId, cancellationToken);
}
}
// Publish event synchronously - all receivers will be called
requestum.Publish(new UserCreatedEvent
{
UserId = 123,
Name = "Alice",
Email = "alice@example.com"
});
// Or publish asynchronously - all async receivers will be called
await requestum.PublishAsync(new UserCreatedEvent
{
UserId = 456,
Name = "Bob",
Email = "bob@example.com"
});
Event Handler Requirements: By default, Requestum requires at least one receiver to be registered for each event type. If you try to publish an event without any receivers, it will throw an exception. You can change this behavior:
services.AddRequestum(cfg =>
{
// Allow publishing events without receivers (silent no-op)
cfg.RequireEventHandlers = false;
cfg.RegisterHandlers(typeof(Program).Assembly);
});
Event vs Command:
- Commands have exactly one handler and represent actions
- Events have zero or more receivers and represent facts
- Commands should be named as imperatives (e.g.,
CreateUser) - Events should be named in past tense (e.g.,
UserCreated)
Middleware
Middleware provides a pipeline for cross-cutting concerns like logging, validation, or exception handling.
Synchronous Middleware
public class LogMiddleware<TRequest, TResponse> : IRequestMiddleware<TRequest, TResponse>
{
public TResponse Invoke(TRequest request, RequestNextDelegate<TRequest, TResponse> next)
{
Console.WriteLine($"Middleware before: '{request}'");
var response = next.Invoke(request);
Console.WriteLine($"Middleware after: '{response}'");
return response;
}
}
Asynchronous Middleware
public class ExceptionHandlerMiddleware<TRequest, TResponse> : IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
AsyncRequestNextDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken = default)
{
try
{
return await next.InvokeAsync(request);
}
catch (Exception ex)
{
Console.WriteLine($"Exception caught: '{ex.Message}'");
throw;
}
}
}
Register Middleware
services.AddRequestum(cfg =>
{
cfg.RegisterHandlers(typeof(Program).Assembly);
cfg.RegisterMiddlewares(typeof(Program).Assembly);
});
⚙️ Registration Options
Basic Registration
services.AddRequestum(cfg =>
{
// Scan assembly for handlers (includes event receivers)
cfg.RegisterHandlers(typeof(Program).Assembly);
// Scan assembly for middlewares
cfg.RegisterMiddlewares(typeof(Program).Assembly);
// Or use Default (scans for both handlers and middlewares)
cfg.Default(typeof(Program).Assembly);
// Configure event handler requirements (default is true)
cfg.RequireEventHandlers = false; // Allow events without receivers
});
Advanced Registration
services.AddRequestum(cfg =>
{
// Set global lifetime (default is Transient)
cfg.Lifetime = ServiceLifetime.Scoped;
// Register specific handler with custom lifetime
cfg.RegisterHandler<MyCommandHandler>(ServiceLifetime.Singleton);
// Register specific middleware type
cfg.RegisterMiddleware(typeof(LoggingMiddleware<,>), ServiceLifetime.Transient);
// Scan multiple assemblies
cfg.RegisterHandlers(
typeof(Program).Assembly,
typeof(AnotherAssembly).Assembly
);
});
📊 Performance
Requestum is designed for high performance with minimal overhead. Here are benchmark comparisons:
Command Execution
| Method | Mean | Allocated |
|---|---|---|
| MediatR_Command_ExecuteAsync (Baseline) | 100% | 100% |
| Requestum_Command_ExecuteAsync | ~75% | ~60% |
| Requestum_Command_ExecuteSync | ~50% | ~40% |
Query Execution
| Method | Mean | Allocated |
|---|---|---|
| MediatR_Query_HandleAsync (Baseline) | 100% | 100% |
| Requestum_Query_HandleAsync | ~75% | ~60% |
| Requestum_Query_HandleSync | ~50% | ~40% |
With Middleware Pipeline
| Method | Mean | Allocated |
|---|---|---|
| MediatR_CommandWithMiddleware_ExecuteAsync (Baseline) | 100% | 100% |
| Requestum_CommandWithMiddleware_ExecuteAsync | ~80% | ~70% |
| Requestum_CommandWithMiddleware_ExecuteSync | ~60% | ~50% |
Service Registration
| Method | Mean | Allocated |
|---|---|---|
| MediatR_RegisterServices (Baseline) | 100% | 100% |
| Requestum_RegisterServices_Default | ~85% | ~80% |
| Requestum_RegisterServices_HandlersOnly | ~70% | ~65% |
| Requestum_RegisterServices_Griffiths | ~40% | ~35% |
Key Takeaways:
- ⚡ 20-50% faster execution compared to MediatR
- 💾 30-60% less memory allocation
- 🚀 Synchronous operations provide best performance
- 📦 Selective registration minimizes startup overhead
Benchmarks performed using BenchmarkDotNet on .NET 9 with MemoryDiagnoser
🔄 Migration from MediatR
Before (MediatR)
// Command
public class CreateUserCommand : IRequest { }
// Handler
public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
public async Task Handle(CreateUserCommand request, CancellationToken ct)
{
// Do work
}
}
// Usage
await _mediator.Send(new CreateUserCommand());
After (Requestum)
// Command
public record CreateUserCommand : ICommand;
// Handler
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct = default)
{
// Do work
}
}
// Usage
await _requestum.ExecuteAsync(new CreateUserCommand());
Key Differences
| Aspect | MediatR | Requestum |
|---|---|---|
| Commands | IRequest |
ICommand |
| Queries | IRequest<T> |
IQuery<T> |
| Handlers | IRequestHandler<T, R> |
ICommandHandler<T> or IQueryHandler<T, R> |
| Execution | Send() |
ExecuteAsync<T>() / HandleAsync<T, R>() |
| Middleware | IPipelineBehavior<T, R> |
IRequestMiddleware<T, R> |
🏗️ Architecture
Core Interfaces
// Requests
public interface ICommand : IBaseRequest;
public interface IQuery<TResponse> : IBaseRequest where TResponse : IResponse;
public interface IEventMessage : IBaseRequest;
// Handlers
public interface ICommandHandler<TCommand> where TCommand : ICommand
{
void Execute(TCommand command);
}
public interface IAsyncCommandHandler<TCommand> where TCommand : ICommand
{
Task ExecuteAsync(TCommand command, CancellationToken cancellationToken = default);
}
public interface IQueryHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
where TResponse : IResponse
{
TResponse Handle(TQuery query);
}
public interface IAsyncQueryHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
where TResponse : IResponse
{
Task<TResponse> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}
// Event Receivers
public interface IEventMessageReceiver<TMessage> where TMessage : IEventMessage
{
void Receive(TMessage message);
}
public interface IAsyncEventMessageReceiver<TMessage> where TMessage : IEventMessage
{
Task ReceiveAsync(TMessage message, CancellationToken cancellationToken = default);
}
// Middleware
public interface IRequestMiddleware<TRequest, TResponse>
{
TResponse Invoke(TRequest request, RequestNextDelegate<TRequest, TResponse> next);
}
public interface IAsyncRequestMiddleware<TRequest, TResponse>
{
Task<TResponse> InvokeAsync(
TRequest request,
AsyncRequestNextDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken = default);
}
❓ FAQ
Q: What's the difference between Command and Query?
A: Commands modify state and don't return values. Queries read data and return typed responses. This follows the CQRS pattern.
Q: What's the difference between Command and Event?
A: Commands are actions that tell the system to do something and have exactly one handler. Events are notifications about something that already happened and can have multiple receivers (or none). Commands represent intent, events represent facts.
Q: Can I have multiple handlers for the same request?
A: For commands and queries - no, one request type should have exactly one handler. For events - yes, you can have multiple receivers. Use middleware for cross-cutting concerns that apply to multiple requests.
Q: What happens if I publish an event with no receivers?
A: By default, Requestum will throw an exception if no receivers are registered. Set RequireEventHandlers = false in configuration to allow publishing events without receivers (they will be silently ignored).
📝 License
MIT License © Daniil Pogovorov
Built with ❤️ for clean architecture and CQRS patterns
| Product | Versions 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 was computed. 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. |
-
net8.0
- Microsoft.Extensions.Options (>= 9.0.10)
-
net9.0
- Microsoft.Extensions.Options (>= 9.0.10)
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.2.6 | 154 | 6/13/2026 |
| 1.2.5 | 772 | 12/8/2025 |
| 1.2.4 | 435 | 12/8/2025 |
| 1.2.3 | 214 | 11/25/2025 |
| 1.2.2 | 188 | 11/23/2025 |
| 1.2.1 | 322 | 11/11/2025 |
| 1.2.0 | 225 | 11/5/2025 |
| 1.2.0-beta-3 | 215 | 11/4/2025 |
| 1.2.0-beta-2 | 215 | 11/4/2025 |
| 1.2.0-beta | 212 | 11/4/2025 |
| 1.1.7 | 162 | 10/31/2025 |
| 1.1.6 | 217 | 10/27/2025 |
| 1.1.5 | 202 | 10/27/2025 |
| 1.1.4 | 174 | 10/26/2025 |
| 1.1.2 | 147 | 10/25/2025 |