Trimble.Mediator 0.1.0-preview

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

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 interface
  • IRequestHandler<TRequest, TResponse> - Same interface
  • INotification - Same interface
  • INotificationHandler<TNotification> - Same interface
  • IPipelineBehavior<TRequest, TResponse> - Same interface
  • IMediator.Send() - Same method
  • IMediator.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

  1. Replace package reference:

    
    <PackageReference Include="MediatR" Version="12.x.x" />
    
    
    <PackageReference Include="Trimble.Mediator" Version="0.1.0-preview" />
    
  2. Update namespace imports:

    // Change from
    using MediatR;
    
    // To
    using Trimble.Mediator;
    
  3. Update service registration:

    // Change from
    services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
    
    // To
    services.AddMediator(typeof(Program).Assembly);
    
  4. (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> { }
    
  5. 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

  1. Keep Handlers Focused: Each handler should do one thing well
  2. Use CQRS Interfaces: Leverage ICommand and IQuery for semantic clarity
  3. Inject Dependencies: Use constructor injection for handler dependencies
  4. Handle Exceptions: Wrap handler logic in try-catch when appropriate
  5. Use Behaviors for Cross-Cutting Concerns: Don't repeat logging, validation, etc. in every handler
  6. Keep Requests Immutable: Use read-only properties or init-only setters
  7. Test Handlers Independently: Handlers are just classes - test them directly
  8. Use CancellationToken: Always pass and respect cancellation tokens
  9. Avoid Mediator in Domain Logic: Keep mediator usage in application/infrastructure layers
  10. 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 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. 
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
0.1.0-preview 384 11/18/2025