SkillAgentSearch skills...

Bellows

A lightweight, powerful mediator library for .NET

Install / Use

/learn @Smithing-Systems/Bellows
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Bellows

A mediator library for .NET that decouples senders from receivers using request/response and pub/sub patterns.

10.0+ NuGet Version Build Tests License

Installation

dotnet add package Bellows

Requirements: .NET 10.0 or higher

Getting Started

1. Register Bellows

In your Program.cs:

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

This registers the mediator and automatically discovers all handlers in the specified assembly.

2. Create a Request and Handler

Define a request that returns a response:

using Bellows;

public record GetUserQuery(int UserId) : IRequest<UserResponse>;

public record UserResponse(int Id, string Name, string Email);

Create a handler for the request:

public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserResponse>
{
    private readonly IUserRepository _repository;

    public GetUserQueryHandler(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<UserResponse> Handle(GetUserQuery request, CancellationToken ct)
    {
        var user = await _repository.GetByIdAsync(request.UserId, ct);
        return new UserResponse(user.Id, user.Name, user.Email);
    }
}

3. Send the Request

Inject IMediator and send your request:

public class UsersController : ControllerBase
{
    private readonly IMediator _mediator;

    public UsersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet("{id}")]
    public async Task<UserResponse> GetUser(int id)
    {
        return await _mediator.Send(new GetUserQuery(id));
    }
}

Requests vs Notifications

Requests (One Handler)

Use requests when you need exactly one handler to process a message and return a result.

// Define the request
public record CreateOrderCommand(int CustomerId, decimal Amount) : IRequest<int>;

// Create the handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    public async Task<int> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order { CustomerId = cmd.CustomerId, Amount = cmd.Amount };
        await _db.Orders.AddAsync(order, ct);
        await _db.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Send the request
var orderId = await _mediator.Send(new CreateOrderCommand(123, 99.99m));

Notifications (Multiple Handlers)

Use notifications when you want multiple handlers to react to an event.

// Define the notification
public record OrderCreated(int OrderId, decimal Amount) : INotification;

// Create multiple handlers
public class SendEmailHandler : INotificationHandler<OrderCreated>
{
    public async Task Handle(OrderCreated notification, CancellationToken ct)
    {
        await _emailService.SendConfirmationAsync(notification.OrderId, ct);
    }
}

public class LogOrderHandler : INotificationHandler<OrderCreated>
{
    public async Task Handle(OrderCreated notification, CancellationToken ct)
    {
        _logger.LogInformation("Order {Id} created: ${Amount}",
            notification.OrderId, notification.Amount);
    }
}

// Publish the notification (all handlers execute)
await _mediator.Publish(new OrderCreated(orderId, 99.99m));

Table of Contents

Pipeline Behaviors

Pipeline behaviors wrap around request handlers to add cross-cutting concerns like logging, validation, or caching.

Creating a Behavior

Implement IPipelineBehavior<TRequest, TResponse>:

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Handling {RequestName}", typeof(TRequest).Name);
        var response = await next();
        _logger.LogInformation("Handled {RequestName}", typeof(TRequest).Name);
        return response;
    }
}

Registering Behaviors

Register pipeline behaviors explicitly using AddPipelineBehavior:

// Register as open generic (applies to all requests)
builder.Services.AddBellows(typeof(Program).Assembly);
builder.Services.AddPipelineBehavior(typeof(LoggingBehavior<,>));
builder.Services.AddPipelineBehavior(typeof(ValidationBehavior<,>));

// Order matters - behaviors execute in registration order

Alternatively, closed generic behaviors are auto-registered when included in assemblies passed to AddBellows().

Validation Example

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : 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)
    {
        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

Configuration

Basic Options

Configure notification execution and exception handling:

builder.Services.AddBellows(options =>
{
    // Execution Strategy: How handlers are executed
    // Parallel (default): All handlers run concurrently for better performance
    // Sequential: Handlers run one at a time in registration order
    options.NotificationPublishStrategy = NotificationPublishStrategy.Parallel;

    // Exception Strategy: How exceptions are handled
    // ContinueOnException (default): All handlers run, first exception rethrown
    // StopOnFirstException: Stop immediately on first exception
    // AggregateExceptions: Collect all exceptions into AggregateException
    // SuppressExceptions: Silently suppress all exceptions (use with caution)
    options.NotificationExceptionHandling = NotificationExceptionHandlingStrategy.ContinueOnException;

}, typeof(Program).Assembly);

Choosing the Right Configuration:

  • Parallel + ContinueOnException (default): Best for performance when handlers are independent and you want all to execute despite failures.
  • Sequential + ContinueOnException: Use when handlers have ordering dependencies or you need deterministic exception handling.
  • Sequential + StopOnFirstException: Use when handler order matters and you want to stop immediately on any failure.
  • Parallel + AggregateExceptions: Use when you need to know about all failures that occurred.

Available Options

| Option | Default | Description | |--------|---------|-------------| | NotificationPublishStrategy | Parallel | Parallel or Sequential | | NotificationExceptionHandling | ContinueOnException | How to handle exceptions in notification handlers | | RequestTimeout | null | Global timeout for requests | | ThrowOnMissingHandler | true | Throw exception if no handler found | | MaxConcurrentNotifications | null | Limit concurrent notification handlers |

Exception Handling Strategies

Exception handling behavior varies based on both the exception handling strategy and the execution strategy (parallel vs sequential):

| Strategy | Parallel Execution | Sequential Execution | Recommendation | |----------|-------------------|---------------------|----------------| | ContinueOnException (default) | All handlers run concurrently. If any fail, the first exception thrown (by timing) is rethrown after all complete. | Handlers run one at a time. If a handler fails, subsequent handlers still execute. The first exception encountered is rethrown after all complete. | ✅ Recommended - Good balance between resilience and error visibility | | StopOnFirstException | All handlers start concurrently. The first exception thrown stops further execution and is immediately rethrown. | Handlers run one at a time. The first exception immediately stops execution and is rethrown. Remaining handlers don't execute. | ⚠️ Use with caution - May leave system in inconsistent state by skipping handlers | | AggregateExceptions | All handlers run concurrently. All exceptions are collected and thrown together as AggregateException after all complete. | Handlers run one at a time. All exceptions are collected and thrown together as AggregateException after all complete. | ✅ Recommended - Best for comprehensive error reporting and diagnostics | | SuppressExceptions | All handlers run concurrently. All exceptions are silently suppressed. | Handlers run one at a time. All exceptions are s

View on GitHub
GitHub Stars6
CategoryDevelopment
Updated2mo ago
Forks0

Languages

C#

Security Score

90/100

Audited on Jan 23, 2026

No findings