ControlTowR 1.0.0-beta.2
dotnet add package ControlTowR --version 1.0.0-beta.2
NuGet\Install-Package ControlTowR -Version 1.0.0-beta.2
<PackageReference Include="ControlTowR" Version="1.0.0-beta.2" />
<PackageVersion Include="ControlTowR" Version="1.0.0-beta.2" />
<PackageReference Include="ControlTowR" />
paket add ControlTowR --version 1.0.0-beta.2
#r "nuget: ControlTowR, 1.0.0-beta.2"
#:package ControlTowR@1.0.0-beta.2
#addin nuget:?package=ControlTowR&version=1.0.0-beta.2&prerelease
#tool nuget:?package=ControlTowR&version=1.0.0-beta.2&prerelease
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
cancellationTokenwith[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
| Product | Versions 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. |
-
.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