High-performance, "no-magic" light request handling implementation for .NET 8, 9, and 10.
If you find this library useful, please give it a star on GitHub! It helps more developers discover the project. ⭐
Kovecses.Requests was built with a clear focus on three main goals:
- High Performance: Optimized execution path with pre-compiled factories and native .NET DI resolution during injection.
- No Magic / Transparent Debugging: Direct handler injection. No "invisible" dispatchers or runtime reflection during execution. If you want to see the implementation, just press
F12on the handler in your endpoint. - Clean Architecture with Pipeline Support: Benefit from decoupled cross-cutting concerns (like logging and validation) via a stateless pipeline implementation that fully supports advanced scenarios like Retry policies (Polly) and recovery logic.
Install the package via NuGet:
dotnet add package Kovecses.Requestsusing Kovecses.Requests;
public record GetBooksQuery : IRequest<IEnumerable<BookDto>>;internal sealed class GetBooksHandler(IBookRepository repository)
: IRequestHandler<GetBooksQuery, IEnumerable<BookDto>>
{
public Task<IEnumerable<BookDto>> HandleAsync(GetBooksQuery request, CancellationToken cancellationToken)
=> repository.GetAllAsync(cancellationToken);
}// In Program.cs or Startup configuration
builder.Services.AddRequests<Program>(); // Scans assemblies and registers handlers// Direct injection of the handler - clean, fast, and F12-able!
app.MapGet("books", async (
IRequestHandler<GetBooksQuery, IEnumerable<BookDto>> handler,
CancellationToken cancellationToken) =>
{
var result = await handler.HandleAsync(new GetBooksQuery(), cancellationToken);
return Results.Ok(result);
});When injecting handlers into Singleton services (like BackgroundService), always use IServiceScopeFactory to resolve the handler within a new scope. This prevents captive dependencies and ensures the Transient pipeline behaves correctly.
using var scope = scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<IRequestHandler<MyRequest, MyResponse>>();The IRequestsBuilder provides a fluent API to register your handlers and cross-cutting concerns (behaviors).
Monitoring and observability are essential for modern cloud-native applications. Kovecses.Requests provides built-in support for OpenTelemetry via an optional behavior.
- Enable the behavior:
builder.Services.AddRequests<Program>()
.AddOpenTelemetry(); // Enables ActivitySource-based tracing for all requests- Configure OpenTelemetry to collect the traces:
To actually see the traces, you must register the
Kovecses.Requestssource in your OpenTelemetry configuration:
services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("Kovecses.Requests") // <-- Required to collect library traces
.AddAspNetCoreInstrumentation()
.AddConsoleExporter());This registers a global OpenTelemetryBehavior that automatically starts an Activity for each request. It captures the request type and handles exception reporting.
builder.Services.AddRequests<Program>()
// 1. Global Behavior: Applies to EVERY request
.AddGlobalBehavior(typeof(LoggingBehavior<,>))
// 2. Interface-based Behavior: Applies only to requests implementing IValidatable marker interface
// Example: public record UpdateBookCommand : IRequest<BookDto>, IValidatable;
.AddBehavior<IValidatable>(typeof(ValidationBehavior<,>))
// 3. Explicit Behavior: Applies ONLY to this specific request
.AddBehavior<GetBooksQuery, IEnumerable<BookDto>, ActiveOnlyBehavior>();Note: All behaviors are registered with Transient lifetime to ensure thread safety and avoid accidental state sharing in the pipeline. This also optimizes performance as the DI container can resolve them efficiently.
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
logger.LogInformation("Starting request {RequestName}", typeof(TRequest).Name);
// The cancellation token is automatically propagated through the pipeline closure
var response = await next();
logger.LogInformation("Finished request {RequestName}", typeof(TRequest).Name);
return response;
}
}The following benchmarks compare Kovecses.Requests with MediatR using BenchmarkDotNet.
Environment: .NET 10.0.1, macOS 26.6 (Darwin 25.6.0), Apple M1
Disclaimer: This comparison is not "apples-to-apples" in terms of features. MediatR is a feature-rich library with a central dispatcher and dynamic resolution. Kovecses.Requests intentionally opts for direct injection and native DI resolution. These benchmarks illustrate the "infrastructure tax" (runtime overhead) you can avoid by choosing a no-magic, direct-injection approach.
Measures resolving a handler from DI and executing it.
| Method | Mean | Ratio | Allocated |
|---|---|---|---|
| DirectCall | 7.831 ns | 1.00 | 72 B |
| KovecsesRequests | 56.332 ns | 7.19 | 168 B |
| MediatR | 81.454 ns | 10.40 | 200 B |
Measures resolving a handler with a pipeline (2 behaviors) and executing it.
| Method | Mean | Ratio | Allocated |
|---|---|---|---|
| DirectCall | 8.155 ns | 1.00 | 72 B |
| KovecsesRequests_With2Behaviors | 139.091 ns | 17.06 | 672 B |
| MediatR_With2Behaviors | 177.913 ns | 21.82 | 728 B |
Note: In these benchmarks, KovecsesRequests includes manual DI resolution (GetRequiredService) to accurately reflect the overhead in a real-world Minimal API endpoint.
This project is licensed under the MIT License.