EasyRequestHandler 1.2.0
dotnet add package EasyRequestHandler --version 1.2.0
NuGet\Install-Package EasyRequestHandler -Version 1.2.0
<PackageReference Include="EasyRequestHandler" Version="1.2.0" />
<PackageVersion Include="EasyRequestHandler" Version="1.2.0" />
<PackageReference Include="EasyRequestHandler" />
paket add EasyRequestHandler --version 1.2.0
#r "nuget: EasyRequestHandler, 1.2.0"
#:package EasyRequestHandler@1.2.0
#addin nuget:?package=EasyRequestHandler&version=1.2.0
#tool nuget:?package=EasyRequestHandler&version=1.2.0
Easy Request Handler
EasyRequestHandler is a lightweight .NET library for request and event handling. It implements the Mediator pattern and an Event Dispatcher on top of Microsoft's built-in Dependency Injection, so you get clean separation of concerns with zero external dependencies beyond Microsoft.Extensions.DependencyInjection.
Target: .NET Standard 2.1 | License: MIT
Installation
dotnet add package EasyRequestHandler
Or via Package Manager Console:
Install-Package EasyRequestHandler
Quick Start
1. Define a request, response, and handler
public class CalculateRequest
{
public int X { get; set; }
public int Y { get; set; }
}
public class CalculateResponse
{
public int Sum { get; set; }
}
public class CalculateHandler : RequestHandler<CalculateRequest, CalculateResponse>
{
public override Task<CalculateResponse> HandleAsync(
CalculateRequest request, CancellationToken cancellationToken = default)
{
return Task.FromResult(new CalculateResponse { Sum = request.X + request.Y });
}
}
2. Register in the DI container
services.AddEasyRequestHandlers(typeof(Program))
.WithMediatorPattern()
.Build();
3. Use it
public class CalculatorController : ControllerBase
{
private readonly ISender _sender;
public CalculatorController(ISender sender) => _sender = sender;
[HttpGet("calculate")]
public async Task<IActionResult> Calculate(int x, int y)
{
var result = await _sender.SendAsync<CalculateRequest, CalculateResponse>(
new CalculateRequest { X = x, Y = y });
return Ok(result);
}
}
That's it. The handler is automatically discovered, registered, and ready to use.
Request Handling
Two ways to invoke a handler
Direct Injection — inject the handler class and call HandleAsync():
app.MapGet("/forecast", async (WeatherForecastHandler handler, CancellationToken ct) =>
{
return await handler.HandleAsync(ct);
});
Mediator Pattern — inject ISender for loose coupling:
app.MapGet("/forecast/{city}", async (string city, ISender sender, CancellationToken ct) =>
{
return await sender.SendAsync<string, WeatherForecast?>(city, cancellationToken: ct);
});
Request handler with input
public class MyRequestHandler : RequestHandler<MyRequest, MyResponse>
{
public override Task<MyResponse> HandleAsync(
MyRequest request, CancellationToken cancellationToken = default)
{
return Task.FromResult(new MyResponse { Result = request.Number * 2 });
}
}
Request handler without input
When a handler needs no input, extend RequestHandler<TResponse>:
public class AllForecastsHandler : RequestHandler<List<WeatherForecast>>
{
public override Task<List<WeatherForecast>> HandleAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(GetForecasts());
}
}
Invoke it through ISender:
var forecasts = await sender.SendAsync<List<WeatherForecast>>();
The Empty response type
When a handler performs an action but has no meaningful return value, use Empty as the response type instead of inventing a dummy class:
public class CreateForecastHandler : RequestHandler<CreateForecastCommand, Empty>
{
public override Task<Empty> HandleAsync(
CreateForecastCommand request, CancellationToken cancellationToken = default)
{
// ... create the forecast
return Task.FromResult(Empty.Value);
}
}
Pipeline Behaviors
Behaviors wrap handler execution like middleware. They run in the order they are registered, forming a pipeline around the handler:
Request --> Behavior 1 --> Behavior 2 --> Handler --> Behavior 2 --> Behavior 1 --> Response
Defining a behavior
Implement IPipelineBehavior<TRequest, TResponse>. Call next() to pass control to the next behavior (or the handler if there are no more behaviors):
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
_logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("Handled {RequestType}", typeof(TRequest).Name);
return response;
}
}
Registering behaviors
Register one or more behaviors in the builder. Order matters — behaviors execute in registration order:
services.AddEasyRequestHandlers(typeof(Program))
.WithMediatorPattern()
.WithBehaviors(
typeof(LoggingBehavior<,>),
typeof(AuthenticationBehavior<,>),
typeof(ValidationBehavior<,>)
)
.Build();
You can also register a single behavior with .WithBehavior(typeof(LoggingBehavior<,>)).
Note: Behaviors require the mediator pattern. Calling
.WithBehaviors()before.WithMediatorPattern()throws anInvalidOperationException.
Request Hooks
Hooks let you run logic before and/or after a handler executes. Unlike behaviors, hooks don't wrap the handler — they run at fixed points in the pipeline. All hooks in the assembly are auto-discovered when enabled.
Three hook types
Full hook — runs both before and after the handler:
public class AuditHook : IRequestHook<MyRequest, MyResponse>
{
public Task OnExecutingAsync(MyRequest request, CancellationToken cancellationToken)
{
// Runs before the handler
return Task.CompletedTask;
}
public Task OnExecutedAsync(MyRequest request, MyResponse response, CancellationToken cancellationToken)
{
// Runs after the handler
return Task.CompletedTask;
}
}
Pre-hook only:
public class ValidationPreHook : IRequestPreHook<MyRequest>
{
public Task OnExecutingAsync(MyRequest request, CancellationToken cancellationToken)
{
// Validate or enrich the request before handling
return Task.CompletedTask;
}
}
Post-hook only:
public class NotificationPostHook : IRequestPostHook<MyRequest, MyResponse>
{
public Task OnExecutedAsync(MyRequest request, MyResponse response, CancellationToken cancellationToken)
{
// React to the result after handling
return Task.CompletedTask;
}
}
Enabling hooks
services.AddEasyRequestHandlers(typeof(Program))
.WithMediatorPattern()
.WithRequestHooks()
.Build();
Hooks are auto-discovered from the scanned assemblies. No manual registration needed.
Execution order
When both behaviors and hooks are enabled, the full pipeline is:
Pre-hooks --> Behaviors --> Handler --> Post-hooks
Event Handling
The event system allows you to publish an event and have it handled by multiple independent handlers — useful for notifications, audit logging, cache invalidation, and other side effects.
Defining events and handlers
An event is any class. Handlers implement IEventHandler<TEvent>:
public class OrderPlacedEvent
{
public Guid OrderId { get; set; }
public string CustomerEmail { get; set; }
}
public class SendConfirmationEmail : IEventHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken)
{
// Send email...
}
}
public class UpdateInventory : IEventHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken)
{
// Adjust stock levels...
}
}
Registering event handlers
services.AddEasyEventHandlers(typeof(Program));
All IEventHandler<> implementations in the assembly are auto-discovered and registered.
Publishing events
Inject IEventPublisher and call PublishAsync:
public class OrderService
{
private readonly IEventPublisher _publisher;
public OrderService(IEventPublisher publisher) => _publisher = publisher;
public async Task PlaceOrder(Order order)
{
// ... save the order
await _publisher.PublishAsync(new OrderPlacedEvent
{
OrderId = order.Id,
CustomerEmail = order.Email
});
}
}
Execution modes
PublishAsync has two overloads: a simple one with defaults, and one that accepts EventPublishOptions for full control:
// Simple — sequential, awaited (default behavior)
await publisher.PublishAsync(myEvent);
// With options — use EventPublishOptions to configure execution
await publisher.PublishAsync(myEvent, new EventPublishOptions
{
UseParallelExecution = true
});
// Fire-and-forget — dispatches to background, returns immediately
await publisher.PublishAsync(myEvent, new EventPublishOptions
{
FireAndForget = true
});
// Fire-and-forget with timeout — cancels background handlers if they exceed the limit
await publisher.PublishAsync(myEvent, new EventPublishOptions
{
FireAndForget = true,
FireAndForgetTimeout = TimeSpan.FromSeconds(30)
});
// Stop on error — if a handler fails, subsequent handlers are skipped (sequential only)
await publisher.PublishAsync(myEvent, new EventPublishOptions
{
StopOnError = true
});
EventPublishOptions
| Property | Default | Behavior |
|---|---|---|
UseParallelExecution |
false |
false: handlers run one after another. true: all handlers start concurrently. |
FireAndForget |
false |
true: returns immediately, handlers run in the background. false: awaits all handlers before returning. |
StopOnError |
false |
true: stops sequential execution at the first handler failure. Ignored when UseParallelExecution is true. |
FireAndForgetTimeout |
null |
When set, cancels background handlers that exceed the specified duration. Only applies when FireAndForget is true. |
Error resilience
By default, a failing handler does not stop the others. In both sequential and parallel modes, all handlers get a chance to execute. Each error is logged individually. When fireAndForget is false, an AggregateException containing all handler errors is thrown after all handlers complete.
When stopOnError is true and handlers run sequentially, the pipeline halts at the first failure — remaining handlers are skipped. This is useful for ordered workflows where later steps depend on earlier ones (e.g., UpdateOrder → SendEmail → SendWhatsApp — if the order update fails, notifications should not be sent). The error from the failed handler is still thrown as an AggregateException.
Fire-and-forget safety
When fireAndForget is true, the caller's CancellationToken is intentionally not forwarded to the background handlers. In ASP.NET Core, the request token is cancelled as soon as the response is sent — since fire-and-forget returns immediately, forwarding it would cancel the background work almost instantly.
To prevent background handlers from running indefinitely, use FireAndForgetTimeout to set a maximum duration:
// Handlers have up to 30 seconds to complete; cancelled after that
await publisher.PublishAsync(orderEvent, new EventPublishOptions
{
FireAndForget = true,
FireAndForgetTimeout = TimeSpan.FromSeconds(30)
});
If the timeout is exceeded, a warning is logged and the remaining work is cancelled. When no timeout is specified (null), handlers run without a time limit.
Handler ordering
By default, handlers execute in registration order. Use [HandlerOrder] to control priority — lower values run first:
[HandlerOrder(1)]
public class AuditHandler : IEventHandler<OrderPlacedEvent>
{
public Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken)
{
// Runs first
return Task.CompletedTask;
}
}
[HandlerOrder(2)]
public class NotificationHandler : IEventHandler<OrderPlacedEvent>
{
public Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken)
{
// Runs second
return Task.CompletedTask;
}
}
Handlers without [HandlerOrder] run after all ordered handlers, preserving their registration order.
Handler lifetime
Event handlers default to Transient. Use [HandlerLifetime] to change it:
[HandlerLifetime(ServiceLifetime.Singleton)]
public class CacheInvalidationHandler : IEventHandler<ProductUpdatedEvent>
{
public Task HandleAsync(ProductUpdatedEvent @event, CancellationToken cancellationToken)
{
// This instance is reused across all event dispatches
return Task.CompletedTask;
}
}
Note: Event handlers always execute in a fresh DI scope created by the publisher, regardless of their registered lifetime. This means scoped services injected into handlers are independent from the caller's scope.
Full Registration Example
var builder = WebApplication.CreateBuilder(args);
// Request handlers with mediator, behaviors, and hooks
builder.Services.AddEasyRequestHandlers(typeof(Program))
.WithMediatorPattern()
.WithBehaviors(
typeof(LoggingBehavior<,>),
typeof(AuthenticationBehavior<,>),
typeof(ValidationBehavior<,>)
)
.WithRequestHooks()
.Build();
// Event handlers (auto-discovered)
builder.Services.AddEasyEventHandlers(typeof(Program));
var app = builder.Build();
// Direct handler injection
app.MapGet("/forecasts", async (AllForecastsHandler handler, CancellationToken ct) =>
{
return await handler.HandleAsync(ct);
});
// Mediator pattern
app.MapGet("/forecast/{city}", async (string city, ISender sender, CancellationToken ct) =>
{
var result = await sender.SendAsync<string, WeatherForecast?>(city, cancellationToken: ct);
return result is not null ? Results.Ok(result) : Results.NotFound();
});
// Event publishing
app.MapPost("/notification", async (NotificationEvent @event, IEventPublisher publisher) =>
{
await publisher.PublishAsync(@event, new EventPublishOptions { UseParallelExecution = true });
return Results.NoContent();
});
app.Run();
Why EasyRequestHandler?
vs MediatR
- No marker interfaces — your request types stay clean (no
IRequest<T>) - Built-in hooks — pre/post execution without additional packages
- Event publisher included — fire-and-forget, parallel execution, handler ordering, error resilience, and stop-on-error for sequential workflows
- Smaller footprint — fewer dependencies, less ceremony
vs hand-rolled solutions
- Auto-discovery — handlers are found by assembly scanning, no manual wiring
- Pipeline behaviors — cross-cutting concerns without repetition
- Tested patterns — mediator, event dispatcher, and DI integration out of the box
Use Cases
- CQRS — separate command and query handling with clear boundaries
- Clean Architecture — enforce separation of concerns with handler-per-feature
- Event-Driven Systems — publish domain events and react asynchronously
- Microservices — standardize request/event handling across services
- API Development — build maintainable APIs with consistent patterns
Getting Help
- Documentation & Source
- Issues — report bugs or request features
- Discussions — questions and ideas
Contributions are welcome! Feel free to submit a Pull Request.
License
Licensed under MIT License.
Made with care by Juan Carlos Torres Cuervo
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
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.0 | 309 | 3/14/2026 |
| 1.1.5 | 137 | 2/16/2026 |
| 1.1.3 | 1,091 | 7/23/2025 |
| 1.1.2 | 248 | 7/16/2025 |
| 1.1.1 | 221 | 7/8/2025 |
| 1.1.0 | 219 | 7/7/2025 |
| 1.0.9 | 217 | 7/7/2025 |
| 1.0.8 | 178 | 7/5/2025 |
| 1.0.7 | 452 | 4/10/2025 |
| 1.0.6 | 237 | 4/10/2025 |
| 1.0.4 | 1,569 | 9/2/2024 |
| 1.0.3 | 200 | 8/30/2024 |
| 1.0.2 | 192 | 8/30/2024 |
| 1.0.1 | 185 | 8/30/2024 |
| 1.0.0 | 212 | 8/30/2024 |