ControlTowR 1.0.0-beta.2

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

ControlTowR

A MediatR-compatible CQRS/Mediator library for .NET — built from scratch, targeting netstandard2.1.

Installation

dotnet add package ControlTowR.Extensions.Microsoft.DependencyInjection

This pulls in the core ControlTowR package automatically. If you only need the interfaces (e.g. for a custom DI integration), install the core package directly:

dotnet add package ControlTowR

Getting Started

1. Register with DI

services.AddControlTowR(typeof(MyHandler).Assembly);

2. Define a request and handler

// Query (returns a value)
public record GetUserQuery(int Id) : IRequest<User>;

public class GetUserQueryHandler : IRequestHandler<GetUserQuery, User>
{
    public Task<User> Handle(GetUserQuery request, CancellationToken cancellationToken)
        => userRepository.GetByIdAsync(request.Id, cancellationToken);
}

// Command (no return value)
public record DeleteUserCommand(int Id) : IRequest;

public class DeleteUserCommandHandler : IRequestHandler<DeleteUserCommand>
{
    public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken)
        => await userRepository.DeleteAsync(request.Id, cancellationToken);
}

3. Send requests

public class UserController(IMediator mediator)
{
    public Task<User> GetUser(int id, CancellationToken ct)
        => mediator.Send(new GetUserQuery(id), ct);

    public Task DeleteUser(int id, CancellationToken ct)
        => mediator.Send(new DeleteUserCommand(id), ct);
}

Notifications

Broadcast a message to multiple handlers:

public record UserCreated(string Username) : INotification;

public class SendWelcomeEmail : INotificationHandler<UserCreated>
{
    public Task Handle(UserCreated notification, CancellationToken cancellationToken)
        => emailService.SendWelcomeAsync(notification.Username, cancellationToken);
}

// Publish
await mediator.Publish(new UserCreated("alice"), cancellationToken);

Handlers are invoked sequentially by default. Multiple handlers for the same notification are all invoked.

Pipeline Behaviors

Add cross-cutting concerns (logging, validation, caching) without modifying handlers:

public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
        var response = await next(ct);
        logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
        return response;
    }
}

Register as an open generic to apply to all requests:

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

Or let assembly scanning handle it automatically if the behavior is in a scanned assembly.

Behaviors execute in registration order — first registered is the outermost wrapper.

Streaming

For handlers that produce multiple results over time:

public record SearchProductsStream(string Query) : IStreamRequest<Product>;

public class SearchProductsStreamHandler : IStreamRequestHandler<SearchProductsStream, Product>
{
    public async IAsyncEnumerable<Product> Handle(
        SearchProductsStream request,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (var product in repository.StreamAsync(request.Query, cancellationToken))
            yield return product;
    }
}

// Consume
await foreach (var product in mediator.CreateStream(new SearchProductsStream("laptop"), ct))
{
    Console.WriteLine(product.Name);
}

Always decorate cancellationToken with [EnumeratorCancellation] in stream handler implementations.

Exception Handling

Exception Handler (suppress and return fallback)

public class OutOfStockHandler
    : IRequestExceptionHandler<PlaceOrderCommand, OrderResult, InsufficientStockException>
{
    public Task Handle(PlaceOrderCommand request, InsufficientStockException exception,
        RequestExceptionHandlerState<OrderResult> state, CancellationToken cancellationToken)
    {
        state.SetHandled(new OrderResult(Success: false, Reason: "Item out of stock"));
        return Task.CompletedTask;
    }
}

Exception Action (side-effect, exception still propagates)

public class DatabaseExceptionLogger<TRequest>
    : IRequestExceptionAction<TRequest, DatabaseException> where TRequest : notnull
{
    public Task Execute(TRequest request, DatabaseException exception, CancellationToken cancellationToken)
    {
        logger.LogError(exception, "Database error in {Request}", typeof(TRequest).Name);
        return Task.CompletedTask;
    }
}

Register the built-in behavior to enable exception handling:

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestExceptionProcessorBehavior<,>));

Assembly Scanning

AddControlTowR scans assemblies and auto-registers all handlers, behaviors, and exception types:

// Single assembly
services.AddControlTowR(typeof(MyHandler).Assembly);

// Multiple assemblies
services.AddControlTowR([typeof(MyHandler).Assembly, typeof(OtherHandler).Assembly]);

// With type filter (useful to exclude test fixtures)
services.AddControlTowR(config => config
    .AddAssembly(typeof(MyHandler).Assembly)
    .Where(t => t.Namespace?.StartsWith("MyApp") == true));

Registered lifetimes: | Type | Lifetime | |------|----------| | IRequestHandler, INotificationHandler, IStreamRequestHandler | Scoped | | IPipelineBehavior, IStreamPipelineBehavior, IRequestExceptionHandler, IRequestExceptionAction | Transient |

Feedback & Bug Reports

Found a bug or have a feature request? Please open an issue on GitHub.

When reporting a bug, include:

  • The version of ControlTowR you're using
  • A minimal reproduction (request type, handler, registration code)
  • The expected vs. actual behaviour

License

GPLv3

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.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ControlTowR:

Package Downloads
ControlTowR.Extensions.Microsoft.DependencyInjection

A MediatR-compatible CQRS/Mediator library for .NET

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-beta.2 151 4/9/2026
1.0.0-beta.1 57 4/9/2026

## v1.0.0-beta.2
* Fixed `InvalidCastException` when a void handler (`IRequestHandler<TRequest>`) is implemented as `async Task` and throws an exception
* Added a Build.Tasks project with a custom MSBuild task to generate the NuGet package's release notes from the CHANGELOG.md file
* Added README to the NuGet package