Trimble.Mediator
0.1.0-preview
dotnet add package Trimble.Mediator --version 0.1.0-preview
NuGet\Install-Package Trimble.Mediator -Version 0.1.0-preview
<PackageReference Include="Trimble.Mediator" Version="0.1.0-preview" />
<PackageVersion Include="Trimble.Mediator" Version="0.1.0-preview" />
<PackageReference Include="Trimble.Mediator" />
paket add Trimble.Mediator --version 0.1.0-preview
#r "nuget: Trimble.Mediator, 0.1.0-preview"
#:package Trimble.Mediator@0.1.0-preview
#addin nuget:?package=Trimble.Mediator&version=0.1.0-preview&prerelease
#tool nuget:?package=Trimble.Mediator&version=0.1.0-preview&prerelease
Trimble.Mediator
An internal MediatR alternative providing request/response, CQRS, and event notification patterns with pipeline behaviors and dependency injection support.
Overview
Trimble.Mediator is a lightweight library that implements the mediator pattern, enabling loosely-coupled in-process messaging. It provides:
- Request/Response Pattern: Send requests to single handlers
- CQRS Support: Explicit command and query interfaces for semantic clarity
- Notifications (Pub/Sub): Publish events to multiple handlers (fire-and-forget)
- Pipeline Behaviors: Cross-cutting concerns like logging, validation, and performance monitoring
- Dependency Injection: First-class support for Microsoft.Extensions.DependencyInjection
Installation
Add the package to your project:
dotnet add package Trimble.Mediator
Or via Package Manager:
Install-Package Trimble.Mediator
Target Frameworks
- .NET 9.0
- .NET 8.0
- .NET 7.0
- .NET Standard 2.1
Getting Started
1. Register Mediator Services
In your Program.cs or Startup.cs:
using Trimble.Mediator;
// Register mediator and scan assemblies for handlers
builder.Services.AddMediator(typeof(Program).Assembly);
// Or with configuration
builder.Services.AddMediator(config =>
{
config.RegisterServicesFromAssembly(typeof(Program).Assembly);
config.RegisterServicesFromAssembly(typeof(OtherAssembly).Assembly);
});
2. Create a Command
Commands modify state and may or may not return data:
using Trimble.Mediator;
// Command without response
public class CreateUserCommand : ICommand
{
public string Username { get; set; }
public string Email { get; set; }
}
// Command with response
public class CreateOrderCommand : ICommand<int>
{
public string CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
}
3. Create a Command Handler
using Trimble.Mediator;
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUserRepository _userRepository;
public CreateUserCommandHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<Unit> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User
{
Username = request.Username,
Email = request.Email
};
await _userRepository.AddAsync(user, cancellationToken);
return Unit.Value;
}
}
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
private readonly IOrderRepository _orderRepository;
public CreateOrderCommandHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order
{
CustomerId = request.CustomerId,
Items = request.Items
};
await _orderRepository.AddAsync(order, cancellationToken);
return order.Id;
}
}
4. Create a Query
Queries are read-only and always return data:
using Trimble.Mediator;
public class GetUserByIdQuery : IQuery<UserDto>
{
public int UserId { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
5. Create a Query Handler
using Trimble.Mediator;
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserRepository _userRepository;
public GetUserByIdQueryHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null)
throw new NotFoundException($"User with ID {request.UserId} not found");
return new UserDto
{
Id = user.Id,
Username = user.Username,
Email = user.Email
};
}
}
6. Send Commands and Queries
using Trimble.Mediator;
public class UserController : ControllerBase
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserCommand command)
{
await _mediator.Send(command);
return Ok();
}
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(new { OrderId = orderId });
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var query = new GetUserByIdQuery { UserId = id };
var user = await _mediator.Send(query);
return Ok(user);
}
}
Notifications (Events)
Notifications are published to multiple handlers in a fire-and-forget manner. Handlers execute in parallel, and exceptions are swallowed (client responsibility).
1. Create a Notification
using Trimble.Mediator;
public class UserCreatedNotification : INotification
{
public int UserId { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
2. Create Notification Handlers
using Trimble.Mediator;
// Handler 1: Send welcome email
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
private readonly IEmailService _emailService;
public SendWelcomeEmailHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
await _emailService.SendWelcomeEmailAsync(notification.Email, cancellationToken);
}
}
// Handler 2: Log user creation
public class LogUserCreationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly ILogger<LogUserCreationHandler> _logger;
public LogUserCreationHandler(ILogger<LogUserCreationHandler> logger)
{
_logger = logger;
}
public Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation("User created: {UserId} - {Username}",
notification.UserId, notification.Username);
return Task.CompletedTask;
}
}
// Handler 3: Update analytics
public class UpdateAnalyticsHandler : INotificationHandler<UserCreatedNotification>
{
private readonly IAnalyticsService _analyticsService;
public UpdateAnalyticsHandler(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
await _analyticsService.TrackUserCreationAsync(notification.UserId, cancellationToken);
}
}
3. Publish Notifications
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUserRepository _userRepository;
private readonly IMediator _mediator;
public CreateUserCommandHandler(IUserRepository userRepository, IMediator mediator)
{
_userRepository = userRepository;
_mediator = mediator;
}
public async Task<Unit> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User
{
Username = request.Username,
Email = request.Email
};
await _userRepository.AddAsync(user, cancellationToken);
// Publish notification - fire and forget
await _mediator.Publish(new UserCreatedNotification
{
UserId = user.Id,
Username = user.Username,
Email = user.Email
}, cancellationToken);
return Unit.Value;
}
}
Pipeline Behaviors
Pipeline behaviors allow you to add cross-cutting concerns that wrap handler execution. Behaviors execute in the order they are registered.
1. Create a Logging Behavior
using Trimble.Mediator;
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)
{
var requestName = typeof(TRequest).Name;
_logger.LogInformation("Handling {RequestName}", requestName);
var response = await next();
_logger.LogInformation("Handled {RequestName}", requestName);
return response;
}
}
2. Create a Performance Monitoring Behavior
using System.Diagnostics;
using Trimble.Mediator;
public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;
public PerformanceBehavior(ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var response = await next();
stopwatch.Stop();
var requestName = typeof(TRequest).Name;
var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
if (elapsedMilliseconds > 500)
{
_logger.LogWarning(
"Long running request: {RequestName} took {ElapsedMilliseconds}ms",
requestName, elapsedMilliseconds);
}
return response;
}
}
3. Create a Validation Behavior
using Trimble.Mediator;
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)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
{
var errors = failures
.GroupBy(f => f.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(f => f.ErrorMessage).ToArray());
throw new ValidationException(errors);
}
return await next();
}
}
4. Create a Transaction Behavior
using Trimble.Mediator;
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : ICommand<TResponse>
{
private readonly IDbContext _dbContext;
public TransactionBehavior(IDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
await using var transaction = await _dbContext.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}
5. Register Behaviors
Behaviors are registered automatically when scanning assemblies. They execute in registration order:
builder.Services.AddMediator(typeof(Program).Assembly);
// Behaviors will execute in this order:
// 1. LoggingBehavior (first discovered)
// 2. PerformanceBehavior (second discovered)
// 3. ValidationBehavior (third discovered)
// 4. TransactionBehavior (last discovered)
// 5. Actual Handler
Migration from MediatR
Trimble.Mediator is designed to be largely compatible with MediatR. Here are the key differences:
Similarities
IRequest<TResponse>- Same interfaceIRequestHandler<TRequest, TResponse>- Same interfaceINotification- Same interfaceINotificationHandler<TNotification>- Same interfaceIPipelineBehavior<TRequest, TResponse>- Same interfaceIMediator.Send()- Same methodIMediator.Publish()- Same method
Differences
| Feature | MediatR | Trimble.Mediator |
|---|---|---|
| CQRS Interfaces | Not provided (community pattern) | Built-in ICommand, ICommand<T>, IQuery<T> |
| Notification Execution | Sequential by default | Parallel fire-and-forget |
| Exception Handling (Notifications) | Throws on first failure | Swallows all exceptions |
| Streaming Requests | IStreamRequest<T> supported |
Not yet supported (planned) |
| Request Pre/Post Processors | Supported | Not yet supported (planned) |
Migration Steps
Replace package reference:
<PackageReference Include="MediatR" Version="12.x.x" /> <PackageReference Include="Trimble.Mediator" Version="0.1.0-preview" />Update namespace imports:
// Change from using MediatR; // To using Trimble.Mediator;Update service registration:
// Change from services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); // To services.AddMediator(typeof(Program).Assembly);(Optional) Use CQRS interfaces:
// Change from public class CreateUserCommand : IRequest { } // To public class CreateUserCommand : ICommand { } // Change from public class GetUserQuery : IRequest<UserDto> { } // To public class GetUserQuery : IQuery<UserDto> { }Update notification handlers for parallel execution (if needed):
- Ensure handlers are thread-safe
- Handle exceptions within handlers (don't let them propagate)
Advanced Usage
Using Base IRequest Without CQRS
You can still use IRequest<TResponse> directly without ICommand or IQuery:
public class MyRequest : IRequest<MyResponse>
{
public string Data { get; set; }
}
public class MyRequestHandler : IRequestHandler<MyRequest, MyResponse>
{
public Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new MyResponse());
}
}
Sending Requests Dynamically
object request = new CreateUserCommand { Username = "john" };
var response = await _mediator.Send(request);
Exception Handling
HandlerNotFoundException
Thrown when no handler is registered for a request:
try
{
await _mediator.Send(new UnregisteredCommand());
}
catch (HandlerNotFoundException ex)
{
// Log or handle missing handler
_logger.LogError(ex, "Handler not found for {RequestType}", ex.RequestType);
}
ValidationException
Custom exception for validation failures:
try
{
await _mediator.Send(new CreateUserCommand { Email = "invalid" });
}
catch (ValidationException ex)
{
// Access validation errors
foreach (var error in ex.Errors)
{
Console.WriteLine($"{error.Key}: {string.Join(", ", error.Value)}");
}
}
Best Practices
- Keep Handlers Focused: Each handler should do one thing well
- Use CQRS Interfaces: Leverage
ICommandandIQueryfor semantic clarity - Inject Dependencies: Use constructor injection for handler dependencies
- Handle Exceptions: Wrap handler logic in try-catch when appropriate
- Use Behaviors for Cross-Cutting Concerns: Don't repeat logging, validation, etc. in every handler
- Keep Requests Immutable: Use read-only properties or init-only setters
- Test Handlers Independently: Handlers are just classes - test them directly
- Use CancellationToken: Always pass and respect cancellation tokens
- Avoid Mediator in Domain Logic: Keep mediator usage in application/infrastructure layers
- Notification Handlers Should Be Independent: Don't rely on execution order or other handlers
Performance Considerations
- Handlers are resolved from DI per request (transient lifetime by default)
- Pipeline behaviors execute in registration order
- Notifications execute in parallel (fire-and-forget)
- Handler lookups are cached using
ConcurrentDictionary - No reflection in hot paths (after first resolution)
Contributing
This is an internal Trimble library. For questions or contributions, contact the platform team.
License
MIT License - © 2025 Trimble
Version: 0.1.0-preview
Target Frameworks: net9.0, net8.0, net7.0, netstandard2.1
Dependencies: Microsoft.Extensions.DependencyInjection.Abstractions >= 8.0.0
| 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 is compatible. 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 is compatible. 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 is compatible. 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
-
net7.0
-
net8.0
-
net9.0
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 |
|---|---|---|
| 0.1.0-preview | 384 | 11/18/2025 |