Bellows 10.0.0

dotnet add package Bellows --version 10.0.0
                    
NuGet\Install-Package Bellows -Version 10.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="Bellows" Version="10.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Bellows" Version="10.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Bellows" />
                    
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 Bellows --version 10.0.0
                    
#r "nuget: Bellows, 10.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 Bellows@10.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=Bellows&version=10.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Bellows&version=10.0.0
                    
Install as a Cake Tool

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 silently suppressed. Not recommended - Hides errors and makes debugging difficult

Key Differences:

  • Parallel + ContinueOnException: The "first" exception is non-deterministic since handlers run concurrently. The exception thrown depends on which handler fails first by timing.
  • Sequential + ContinueOnException: The "first" exception is deterministic - it's the exception from the first handler in registration order that throws.
  • StopOnFirstException with Parallel: Cannot guarantee which handler's exception is thrown due to concurrent execution, only that execution stops as soon as one fails.
  • Sequential Execution: Provides deterministic, predictable ordering for both success and failure cases.

Example:

public record OrderCreated(int OrderId) : INotification;

public class EmailHandler : INotificationHandler<OrderCreated>
{
    public async Task Handle(OrderCreated notification, CancellationToken ct)
    {
        await Task.Delay(50);
        throw new InvalidOperationException("Email service down");
    }
}

public class LogHandler : INotificationHandler<OrderCreated>
{
    public async Task Handle(OrderCreated notification, CancellationToken ct)
    {
        await Task.Delay(10);
        throw new InvalidOperationException("Logging failed");
    }
}

// Parallel + ContinueOnException (default):
// Both handlers run concurrently. LogHandler likely throws first due to shorter delay.
// Result: "Logging failed" exception thrown (non-deterministic)

// Sequential + ContinueOnException:
// EmailHandler runs first (registration order), then LogHandler.
// Result: "Email service down" exception thrown (deterministic)

// Parallel + StopOnFirstException:
// Both start. First failure stops execution immediately.
// Result: Either exception could be thrown (race condition)

// Sequential + StopOnFirstException:
// EmailHandler runs first and throws. LogHandler never executes.
// Result: "Email service down" exception thrown

// Parallel + AggregateExceptions:
// Both run and both exceptions are collected.
// Result: AggregateException containing both exceptions

// Sequential + AggregateExceptions:
// Both run sequentially and both exceptions are collected.
// Result: AggregateException containing both exceptions (in registration order)

API Reference

Core Interfaces

IMediator - Main interface for sending requests and publishing notifications

Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);
Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification;

IRequest<TResponse> - Marker interface for requests

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

IRequestHandler<TRequest, TResponse> - Handler for requests

public class GetUserHandler : IRequestHandler<GetUserQuery, UserResponse>
{
    Task<UserResponse> Handle(GetUserQuery request, CancellationToken cancellationToken);
}

INotification - Marker interface for notifications

public record UserCreated(int UserId) : INotification;

INotificationHandler<TNotification> - Handler for notifications

public class UserCreatedHandler : INotificationHandler<UserCreated>
{
    Task Handle(UserCreated notification, CancellationToken cancellationToken);
}

IPipelineBehavior<TRequest, TResponse> - Cross-cutting concerns

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
}

Made with ❤️ by Smithing Systems

Product Compatible and additional computed target framework versions.
.NET 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
10.0.0 709 1/23/2026
9.0.0 195 10/22/2025
9.0.0-beta 173 10/22/2025
9.0.0-alpha 174 10/20/2025