EasyRequestHandler 1.2.0

dotnet add package EasyRequestHandler --version 1.2.0
                    
NuGet\Install-Package EasyRequestHandler -Version 1.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EasyRequestHandler" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EasyRequestHandler" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="EasyRequestHandler" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EasyRequestHandler --version 1.2.0
                    
#r "nuget: EasyRequestHandler, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EasyRequestHandler@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EasyRequestHandler&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=EasyRequestHandler&version=1.2.0
                    
Install as a Cake Tool

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 an InvalidOperationException.


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., UpdateOrderSendEmailSendWhatsApp — 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

Contributions are welcome! Feel free to submit a Pull Request.


License

Licensed under MIT License.

Made with care by Juan Carlos Torres Cuervo

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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