LightWeightMediator 1.0.0

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package LightWeightMediator --version 1.0.0
                    
NuGet\Install-Package LightWeightMediator -Version 1.0.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="LightWeightMediator" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LightWeightMediator" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="LightWeightMediator" />
                    
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 LightWeightMediator --version 1.0.0
                    
#r "nuget: LightWeightMediator, 1.0.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 LightWeightMediator@1.0.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=LightWeightMediator&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=LightWeightMediator&version=1.0.0
                    
Install as a Cake Tool

LightWeightMediator

A convention-driven, lightweight mediator library that contains only the parts of MediatR you actually need.

Contents: IRequest, IRequestHandler, IMediator, IPipelineBehavior. Nothing else — concepts you don't use, such as notifications, stream requests and RequestPreProcessor, are deliberately left out.

Single dependency: Microsoft.Extensions.DependencyInjection.Abstractions.

Targets: .NET 8 and .NET 10.


Enforced rules

# Rule Where it is caught
1 IRequest can only be a class / record Compile time + startup + runtime
2 Only an IRequest can be passed as the generic argument to IRequestHandler Compile time
3 A request cannot be bound to a second handler Startup (fail-fast)

How is Rule 1 enforced?

C# has no language feature to say "only reference types may implement this interface". So the rule is defended at three layers at once:

  1. Compile time — the where TRequest : class constraint on IRequestHandler<TRequest, TResponse>. If you try to write a handler for a struct request, the project won't compile. So a struct request without a handler can be defined in theory, but can never be executed.
  2. Startup — if a value type implementing IRequest<> is found while scanning assemblies, an InvalidRequestTypeException is thrown. record struct is caught here too.
  3. Runtime — if IMediator.Send is called with a value type, again an InvalidRequestTypeException.

If you want it even stricter: you can write a Roslyn Analyzer that raises a direct compile error on a struct implementing IRequest. The library is safe without it; the analyzer just surfaces the error instantly in the IDE.

How is Rule 2 enforced?

public interface IRequestHandler<in TRequest, TResponse>
    where TRequest : class, IRequest<TResponse>

The constraint guarantees both "a non-request type cannot be passed" and "TResponse must match the response type declared by the request". You cannot write IRequestHandler<string, int>; nor can you write IRequestHandler<CreateOrderCommand, int> for a request that is IRequest<Guid>.

How is Rule 3 enforced?

A Dictionary<requestType, handlerType> is filled during scanning. The moment a second handler for the same request type appears, a DuplicateRequestHandlerException is thrown — the application won't start. It does not wait for the first request; it fails at deploy time.

LightWeightMediator.DuplicateRequestHandlerException:
Multiple handlers were found for request 'Shop.Orders.CreateOrderCommand':
'Shop.Orders.CreateOrderCommandHandler' and 'Shop.Orders.CreateOrderCommandHandlerV2'.
A request can be bound to only a single handler.

Note: the reverse is allowed — a single handler class can handle multiple requests, because the dictionary key is the request type.


Installation

dotnet add package LightWeightMediator

Program.cs:

using LightWeightMediator.Abstractions;
using LightWeightMediator.DependencyInjection;

builder.Services.AddLightWeightMediator(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<Program>();

    // Pipeline order = registration order. The first registered is the outermost.
    cfg.AddOpenBehavior(typeof(CachingBehavior<,>));
    cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
    cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});

Short form:

builder.Services.AddLightWeightMediator(typeof(Program).Assembly);

Configuration options

Member Description
RegisterServicesFromAssembly(assembly) Adds an assembly to the scan list
RegisterServicesFromAssemblies(params ...) Multiple assemblies
RegisterServicesFromAssemblyContaining<T>() The assembly containing the type
AddOpenBehavior(typeof(X<,>)) A behavior for all requests
AddBehavior<T>() A behavior for a single request type
HandlerLifetime Default Transient
ValidateEveryRequestHasHandler If true, a request without a handler fails at startup

Usage

1. Request that returns a response (query)

public sealed record GetUserByIdQuery(Guid Id) : IRequest<UserDto>;

public sealed class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
    private readonly AppDbContext _db;

    public GetUserByIdQueryHandler(AppDbContext db) => _db = db;

    public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
    {
        var user = await _db.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Id == request.Id, cancellationToken);

        return user is null ? throw new KeyNotFoundException() : new UserDto(user.Id, user.Name);
    }
}

2. Request that returns no response (command)

Recommended way — identical to modern MediatR: implement the arity-1 IRequestHandler<TRequest> directly, return a plain Task, no Unit, no base class:

public sealed record DeleteUserCommand(Guid Id) : IRequest;

public sealed class DeleteUserCommandHandler : IRequestHandler<DeleteUserCommand>
{
    public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken)
    {
        // ...
    }
}

The library bridges this plain Task to Unit internally in the pipeline; IPipelineBehavior<TRequest, Unit> behaviors work for void handlers exactly the same way.

<details> <summary>Alternatives (backward compatibility)</summary>

If you prefer to return Unit manually, you can use the arity-2 signature too:

public sealed class DeleteUserCommandHandler : IRequestHandler<DeleteUserCommand, Unit>
{
    public async Task<Unit> Handle(DeleteUserCommand request, CancellationToken cancellationToken)
    {
        // ...
        return Unit.Value;
    }
}

Or the ready-made base class to avoid dealing with Unit:

public sealed class DeleteUserCommandHandler : RequestHandler<DeleteUserCommand>
{
    protected override async Task HandleAsync(DeleteUserCommand request, CancellationToken cancellationToken)
    {
        // ...
    }
}

All three styles work with the same mediator.Send(command) call. </details>

3. Sending

app.MapGet("/users/{id:guid}", async (Guid id, IMediator mediator, CancellationToken ct) =>
{
    var user = await mediator.Send(new GetUserByIdQuery(id), ct);
    return Results.Ok(user);
});

app.MapDelete("/users/{id:guid}", async (Guid id, IMediator mediator, CancellationToken ct) =>
{
    await mediator.Send(new DeleteUserCommand(id), ct);   // returns Task, you never see Unit
    return Results.NoContent();
});

Inside a controller:

public sealed class UsersController : ControllerBase
{
    private readonly IMediator _mediator;

    public UsersController(IMediator mediator) => _mediator = mediator;

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> Get(Guid id, CancellationToken ct)
        => Ok(await _mediator.Send(new GetUserByIdQuery(id), ct));
}

Pipeline Behavior

Exactly the same idea as ASP.NET Core middleware: code runs before and after next(), and if next() is never called the handler does not run (short-circuit).

Send()
  └─ CachingBehavior          (registered 1st → outermost)
       └─ ValidationBehavior  (registered 2nd)
            └─ TransactionBehavior
                 └─ Handler   (innermost)

Validation behavior (with FluentValidation)

public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : class, IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
        {
            return await next();
        }

        var context = new ValidationContext<TRequest>(request);

        var failures = (await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, cancellationToken))))
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Count > 0)
        {
            throw new ValidationException(failures);
        }

        return await next();
    }
}

Transaction behavior

public sealed class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : class, IRequest<TResponse>
{
    private readonly AppDbContext _db;

    public TransactionBehavior(AppDbContext db) => _db = db;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Open a transaction only for commands
        if (request is not ITransactionalRequest)
        {
            return await next();
        }

        await using var transaction = await _db.Database.BeginTransactionAsync(cancellationToken);

        var response = await next();

        await _db.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);

        return response;
    }
}

Defining an empty marker interface like ITransactionalRequest and running the behavior only for those requests saves you from having to "register a separate behavior for each request".

Behavior for a single request

public sealed class CreateOrderAuditBehavior : IPipelineBehavior<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(
        CreateOrderCommand request,
        RequestHandlerDelegate<Guid> next,
        CancellationToken cancellationToken)
    {
        var orderId = await next();
        // audit...
        return orderId;
    }
}

// registration
cfg.AddBehavior<CreateOrderAuditBehavior>();

The rules already fail at startup, but to catch them early in CI:

[Fact]
public void All_handlers_can_be_registered_according_to_the_rules()
{
    var services = new ServiceCollection();

    // If there is a duplicate handler or a struct request, an exception is thrown here
    services.AddLightWeightMediator(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());

    var registry = services.BuildServiceProvider().GetRequiredService<IHandlerRegistry>();

    Assert.NotEmpty(registry.Handlers);
}

With IHandlerRegistry you can also see at runtime which request is bound to which handler (useful for a health check / debug endpoint).


Performance notes

  • Reflection is done once per request type (MakeGenericType), and the result is kept in a static ConcurrentDictionary. All subsequent calls run fully typed.
  • Mediator is transient and stateless; it only carries the IServiceProvider of the scope it lives in.
  • The behavior chain is built on every request (delegate allocation). If profiling shows this is a problem, the chain can be cached per request type too — but measure first.
  • Because it uses reflection, you may get a MakeGenericType warning in Native AOT / trimming scenarios. There is no issue in a classic ASP.NET Core deployment.

Migrating from MediatR

MediatR LightWeightMediator
IRequest<T> IRequest<T> (same)
IRequestHandler<T, TResponse> IRequestHandler<T, TResponse> (same)
IRequestHandler<T> (void, plain Task) IRequestHandler<T> (same)
IMediator / ISender IMediator (same)
IPipelineBehavior<T, R> IPipelineBehavior<T, R> (same)
RequestHandlerDelegate<R> RequestHandlerDelegate<R> (same)
INotification, IStreamRequest None
AddMediatR(cfg => ...) AddLightWeightMediator(cfg => ...)

In practice, changing using MediatR; lines to the LightWeightMediator.* namespaces and swapping the AddMediatR call is enough for most projects.


License

MIT

Product 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 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 is compatible.  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. 
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