Skip to content

Latest commit

Β 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Light.Mediator

A lightweight, high-performance mediator library for .NET β€” implementing the Mediator pattern with CQRS support, pipeline behaviors, and notification publishing.

NuGet .NET Standard

✨ Features

  • CQRS ready β€” Separate ICommand<T>, IQuery<T>, and IRequest<T> contracts
  • Clean void commands β€” Task Handle(), no Unit boilerplate needed
  • Pipeline behaviors β€” Middleware-style IPipelineBehavior<TRequest, TResponse> for cross-cutting concerns
  • Notification publishing β€” Fan-out to multiple INotificationHandler<T> with error resilience
  • Zero dependencies on contracts β€” Light.Mediator.Contracts has no external dependencies
  • High performance β€” Cached wrapper resolution via ConcurrentDictionary, zero-alloc fast-path
  • Safe DI registration β€” Assembly scanning with duplicate protection and safe type loading
  • Targets netstandard2.1

πŸ“¦ Installation

dotnet add package Lightsoft.Mediator

For projects that only define requests/commands/queries (e.g., shared contracts):

dotnet add package Lightsoft.Mediator.Contracts

πŸš€ Quick Start

1. Define a request and handler

using Light.Mediator;

// Query with response
public record GetOrderById(int Id) : IQuery<OrderDto>;

public class GetOrderByIdHandler : IQueryHandler<GetOrderById, OrderDto>
{
    public Task<OrderDto> Handle(GetOrderById request, CancellationToken ct)
        => Task.FromResult(new OrderDto(request.Id, "Sample Order"));
}

// Void command β€” clean, no Unit!
public record DeleteOrder(int Id) : ICommand;

public class DeleteOrderHandler : ICommandHandler<DeleteOrder>
{
    public Task Handle(DeleteOrder request, CancellationToken ct)
        => Task.CompletedTask;
}

2. Register services

using Light.Mediator;
using System.Reflection;

builder.Services.AddMediatorFromAssemblies(Assembly.GetExecutingAssembly());

3. Send requests

public class OrdersController : ControllerBase
{
    private readonly ISender _sender;
    public OrdersController(ISender sender) => _sender = sender;

    [HttpGet("{id}")]
    public async Task<OrderDto> Get(int id)
        => await _sender.Send(new GetOrderById(id));

    [HttpDelete("{id}")]
    public async Task Delete(int id)
        => await _sender.Send(new DeleteOrder(id));
}

πŸ“– Core Concepts

Contracts (Light.Mediator.Contracts)

Interface Purpose Returns
IRequest<TResponse> Base request with typed response TResponse
IRequest Void request (returns Unit) Unit
ICommand<TResponse> Command with response TResponse
ICommand Void command Unit
IQuery<TResponse> Query with response TResponse
INotification Notification (fan-out) β€”

Unit Type

Unit is a readonly struct used internally for void operations. With the non-generic handler interfaces, you rarely need to interact with Unit directly:

// βœ… Void handler β€” clean, no Unit!
public class DeleteOrderHandler : ICommandHandler<DeleteOrder>
{
    public Task Handle(DeleteOrder request, CancellationToken ct)
        => Task.CompletedTask;
}

πŸ”§ Handlers

Request / Command / Query Handlers

// Generic β€” with response
public class MyHandler : IRequestHandler<MyRequest, MyResponse> { ... }
public class MyHandler : ICommandHandler<MyCommand, int> { ... }
public class MyHandler : IQueryHandler<MyQuery, MyDto> { ... }

// Non-generic β€” void (Task Handle(), no Unit!)
public class MyHandler : IRequestHandler<MyRequest> { ... }
public class MyHandler : ICommandHandler<MyCommand> { ... }

Non-generic handlers use Task Handle(...) independently (MediatR-style) β€” the library automatically bridges to Task<Unit> internally via VoidRequestHandlerAdapter.

Notification Handlers

Multiple handlers per notification type β€” all execute sequentially:

public class SendEmailHandler : INotificationHandler<OrderCreated>
{
    public Task Handle(OrderCreated notification, CancellationToken ct)
        => Task.CompletedTask;
}
await mediator.Publish(new OrderCreated(orderId));

πŸ”— Pipeline Behaviors

Add cross-cutting concerns (logging, validation, transactions) as middleware:

// Open generic β€” applies to all requests
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async Task<TResponse> Handle(
        TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        Console.WriteLine($"Handling {typeof(TRequest).Name}");
        var result = await next(ct);
        Console.WriteLine($"Handled {typeof(TRequest).Name}");
        return result;
    }
}

// Closed β€” for specific void command (use shorthand IPipelineBehavior<TRequest>)
public class DeleteAuditBehavior : IPipelineBehavior<DeleteOrder>
{
    public Task<Unit> Handle(
        DeleteOrder request, RequestHandlerDelegate<Unit> next, CancellationToken ct)
    {
        Console.WriteLine($"Auditing delete: {request.Id}");
        return next(ct);
    }
}

Register behaviors β€” they execute in registration order (first registered = outermost):

builder.Services.AddBehaviors(
    typeof(LoggingBehavior<,>),       // outermost
    typeof(ValidationBehavior<,>),    // innermost
    typeof(DeleteAuditBehavior)       // closed β€” auto-resolves to IPipelineBehavior<DeleteOrder, Unit>
);

Pipeline execution order:

LoggingBehavior:Before β†’ ValidationBehavior:Before β†’ Handler β†’ ValidationBehavior:After β†’ LoggingBehavior:After

βš™οΈ DI Registration

Assembly Scanning

builder.Services.AddMediatorFromAssemblies(
    Assembly.GetExecutingAssembly(),
    typeof(SomeHandler).Assembly
);

What gets registered:

  • Mediator as IMediator, ISender, IPublisher β€” via TryAddTransient
  • IRequestHandler<,> implementations β€” TryAddTransient (single handler per request)
  • IRequestHandler<> (void) β€” registers handler + VoidRequestHandlerAdapter bridge
  • INotificationHandler<> implementations β€” AddTransient with duplicate protection

Behavior Registration

builder.Services.AddBehaviors(
    typeof(LoggingBehavior<,>),          // open generic
    typeof(DeleteAuditBehavior)          // closed β€” auto-detects every IPipelineBehavior<,> interface it implements
);

Re-registering the exact same behavior type (same service interface + implementation pair) is a no-op, so calling AddBehaviors more than once for the same type β€” e.g. from two separate composition modules β€” won't double up the pipeline.

πŸ›‘οΈ Error Handling

Notification Error Resilience

  • Non-cancellation exceptions are collected and thrown as AggregateException
  • OperationCanceledException / TaskCanceledException propagate immediately

Request Error Handling

  • Handler exceptions propagate through the pipeline normally
  • Behaviors can catch and handle exceptions via standard try/catch around next()

πŸ—οΈ Project Structure

Mediator.Contracts/          ← Pure contracts, zero dependencies
β”œβ”€β”€ ICommand.cs                     ICommand<T>, ICommand
β”œβ”€β”€ INotification.cs                INotification
β”œβ”€β”€ IQuery.cs                       IQuery<T>
β”œβ”€β”€ IRequest.cs                     IRequest<T>, IRequest
└── Unit.cs                         Unit struct

Mediator/                    ← Core + DI, depends on Contracts
β”œβ”€β”€ Adapters/                       Internal DI-constructed bridges
β”‚   └── VoidRequestHandlerAdapter.cs  Taskβ†’Task<Unit> bridge
β”œβ”€β”€ Wrappers/                       Internal handler/behavior wrappers
β”‚   β”œβ”€β”€ BehaviorWrapper.cs
β”‚   β”œβ”€β”€ HandlerWrapper.cs
β”‚   └── NotificationHandlerWrapper.cs
β”œβ”€β”€ ICommandHandler.cs              ICommandHandler<T,R>, ICommandHandler<T>
β”œβ”€β”€ IMediator.cs                    IMediator : ISender, IPublisher
β”œβ”€β”€ INotificationHandler.cs         INotificationHandler<T>
β”œβ”€β”€ IPipelineBehavior.cs            IPipelineBehavior<T,R>, IPipelineBehavior<T>
β”œβ”€β”€ IPublisher.cs                   IPublisher
β”œβ”€β”€ IQueryHandler.cs                IQueryHandler<T,R>
β”œβ”€β”€ IRequestHandler.cs              IRequestHandler<T,R>, IRequestHandler<T>
β”œβ”€β”€ ISender.cs                      ISender
β”œβ”€β”€ Mediator.cs                     Core mediator implementation
β”œβ”€β”€ RequestHandlerDelegate.cs       RequestHandlerDelegate<T> delegate
└── ServiceCollectionExtensions.cs  DI registration extensions

Why two packages?

  • Contracts β€” Reference only this in shared/domain projects. Zero dependencies, minimal surface.
  • Mediator β€” Reference this in your composition root / startup project. Brings in Microsoft.Extensions.DependencyInjection.

πŸ“„ License

MIT

About

Simple mediator pattern

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages