Moderator.Contracts 0.11.0

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

Moderator

A high-performance, lightweight in-process mediator implementation for .NET applications. Moderator provides a clean abstraction for request/response patterns, event notifications, streaming operations, and cross-cutting concerns through pipeline behaviors.

📦 NuGet Packages

Package Version Downloads Description
Moderator NuGet Downloads Core runtime implementation
Moderator.Contracts NuGet Downloads Interfaces and contracts
Moderator.Generator NuGet Downloads Source generator for compile-time discovery

License: MIT .NET

📋 Table of Contents

✨ Features

  • Simple and Explicit Contracts - Clear interfaces for request handlers and notification handlers
  • Streaming Support - Built-in support for async streaming operations
  • Pipeline Behaviors - Compose cross-cutting concerns with explicit ordering control
  • Zero Value Results - Included Unit type for commands without return values
  • Source Generator - Compile-time discovery of handlers (behaviors registered manually for order control)
  • Minimal Dependencies - Lightweight implementation with no external dependencies
  • Thread-Safe - Fully thread-safe mediator implementation
  • Testable - Easy to mock and test with clear interface contracts
  • Explicit Behavior Ordering - Manual behavior registration ensures predictable execution order

📦 Installation

Package Manager Console

Install-Package Moderator
Install-Package Moderator.Contracts
Install-Package Moderator.Generator  # Optional but recommended

.NET CLI

dotnet add package Moderator
dotnet add package Moderator.Contracts
dotnet add package Moderator.Generator  # Optional but recommended

Package Reference

<PackageReference Include="Moderator" Version="*" />
<PackageReference Include="Moderator.Contracts" Version="*" />
<PackageReference Include="Moderator.Generator" Version="*" />  

🚀 Getting Started

Basic Setup

using Microsoft.Extensions.DependencyInjection;
using Moderator;

var builder = WebApplication.CreateBuilder(args);

// Register handlers (auto-discovered) and behaviors (manual - order matters!)
builder.Services.AddModerator(config =>
{
    // Auto-discovered handlers and notifications
    config.DiscoveredTypes = DiscoveredTypes.All;
    
    // Pipeline behaviors must be manually registered to control execution order
    // They execute in the order registered (outermost to innermost)
    config.Behaviors =
    [
        (typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)),      // Executes first
        (typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)),   // Executes second
        (typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>))   // Executes last (closest to handler)
    ];
});

var app = builder.Build();

// Use without specifying generic types
app.MapPost("/users", async (IModerator moderator, CreateUserRequest request) =>
{
    // Type inference from generated extension methods
    var response = await moderator.SendAsync(request);
    await moderator.PublishAsync(new UserCreatedEvent(response.UserId));
    return Results.Ok(response);
});

app.Run();
Option 2: Manual Registration
using Microsoft.Extensions.DependencyInjection;
using Moderator;

var services = new ServiceCollection();

// Register the mediator
services.AddScoped<IModerator, Moderator>();

// Manually register your handlers
services.AddScoped<IRequestHandler<GetUserQuery, UserDto>, GetUserHandler>();
services.AddScoped<INotificationHandler<UserCreatedEvent>, UserCreatedEventHandler>();

// Register pipeline behaviors - order matters!
// They execute in the order registered (outermost to innermost)
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));      // First
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));   // Second
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));  // Last

var serviceProvider = services.BuildServiceProvider();
var moderator = serviceProvider.GetRequiredService<IModerator>();

Simple Request/Response Example

// Query definition
public sealed record GetUserByIdQuery(Guid UserId);

// Response DTO
public sealed record UserDto(Guid Id, string Name, string Email);

// Handler implementation
public sealed class GetUserByIdHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
    private readonly IUserRepository _repository;

    public GetUserByIdHandler(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<UserDto> HandleAsync(
        GetUserByIdQuery request, 
        CancellationToken cancellationToken = default)
    {
        var user = await _repository.GetByIdAsync(request.UserId, cancellationToken);
        return new UserDto(user.Id, user.Name, user.Email);
    }
}

// Usage with source generator (type inference)
var user = await moderator.SendAsync(
    new GetUserByIdQuery(userId), 
    cancellationToken);

// Usage without source generator (explicit types)
var user = await moderator.SendAsync<GetUserByIdQuery, UserDto>(
    new GetUserByIdQuery(userId), 
    cancellationToken);

📚 Core Concepts

Request/Response

Implement IRequestHandler<TRequest, TResponse> for synchronous request/response patterns:

public interface IRequestHandler<in TRequest, TResponse>
{
    Task<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken = default);
}
Commands (No Return Value)

For commands that don't return a value, use the Unit type:

public sealed record DeleteUserCommand(Guid UserId);

public sealed class DeleteUserHandler : IRequestHandler<DeleteUserCommand, Unit>
{
    private readonly IUserRepository _repository;

    public DeleteUserHandler(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<Unit> HandleAsync(
        DeleteUserCommand request, 
        CancellationToken cancellationToken = default)
    {
        await _repository.DeleteAsync(request.UserId, cancellationToken);
        return Unit.Value;
    }
}

Notifications

Implement INotificationHandler<TNotification> for pub/sub patterns:

// Notification must implement INotification marker interface
public sealed record UserRegisteredEvent(
    Guid UserId, 
    string Email, 
    DateTime RegisteredAt) : INotification;

// Multiple handlers can handle the same notification
public sealed class SendWelcomeEmailHandler : INotificationHandler<UserRegisteredEvent>
{
    private readonly IEmailService _emailService;

    public SendWelcomeEmailHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task HandleAsync(
        UserRegisteredEvent notification, 
        CancellationToken cancellationToken = default)
    {
        await _emailService.SendWelcomeEmailAsync(
            notification.Email, 
            cancellationToken);
    }
}

public sealed class UpdateAnalyticsHandler : INotificationHandler<UserRegisteredEvent>
{
    public async Task HandleAsync(
        UserRegisteredEvent notification, 
        CancellationToken cancellationToken = default)
    {
        // Update analytics
        await Task.CompletedTask;
    }
}

// Publishing notifications
await moderator.PublishAsync(
    new UserRegisteredEvent(userId, email, DateTime.UtcNow), 
    cancellationToken);

Streaming

For progressive data streaming, implement IStreamRequestHandler<TRequest, TResponse>:

public sealed record SearchProductsQuery(
    string SearchTerm, 
    int MaxResults = 100);

public sealed class SearchProductsHandler 
    : IStreamRequestHandler<SearchProductsQuery, ProductSearchResult>
{
    private readonly IProductSearchService _searchService;

    public SearchProductsHandler(IProductSearchService searchService)
    {
        _searchService = searchService;
    }

    public async IAsyncEnumerable<ProductSearchResult> HandleAsync(
        SearchProductsQuery request,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var batch in _searchService.SearchAsync(
            request.SearchTerm, 
            request.MaxResults, 
            cancellationToken))
        {
            foreach (var product in batch)
            {
                yield return new ProductSearchResult(
                    product.Id, 
                    product.Name, 
                    product.Price);
            }
        }
    }
}

// Consuming streams
await foreach (var result in moderator.StreamAsync<SearchProductsQuery, ProductSearchResult>(
    new SearchProductsQuery("laptop", 50), 
    cancellationToken))
{
    Console.WriteLine($"Found: {result.Name} - ${result.Price}");
}

Pipeline Behaviors

Pipeline behaviors wrap handler execution for cross-cutting concerns. Important: Behaviors must be manually registered to control execution order.

Behavior Execution Order

Behaviors execute in the order they are registered (outermost to innermost):

// Registration order determines execution order
builder.Services.AddModerator(config =>
{
    config.DiscoveredTypes = DiscoveredTypes.All;
    
    config.Behaviors =
    [
        (typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)),      // 1st (outermost)
        (typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)),   // 2nd
        (typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>))   // 3rd (innermost)
    ];
});

// Execution flow for a request:
// → LoggingBehavior starts
//   → ValidationBehavior starts  
//     → TransactionBehavior starts
//       → Handler executes
//     ← TransactionBehavior completes
//   ← ValidationBehavior completes
// ← LoggingBehavior completes
Example Pipeline Behavior
public sealed class TimingBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<TimingBehavior<TRequest, TResponse>> _logger;

    public TimingBehavior(ILogger<TimingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken = default)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            // Call the next behavior or handler
            var response = await next(cancellationToken);
            stopwatch.Stop();
            
            if (stopwatch.ElapsedMilliseconds > 500)
            {
                _logger.LogWarning(
                    "Long running request: {RequestName} ({ElapsedMilliseconds}ms)",
                    typeof(TRequest).Name,
                    stopwatch.ElapsedMilliseconds);
            }
            
            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex, 
                "Request failed: {RequestName} ({ElapsedMilliseconds}ms)",
                typeof(TRequest).Name,
                stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
}
Why Manual Registration?

The source generator cannot auto-discover behaviors because:

  1. Order Matters - Execution order affects behavior (e.g., logging before validation)
  2. Conditional Registration - You may want different behaviors in different environments
  3. Explicit Control - Critical infrastructure code should be explicitly configured
Streaming Pipeline Behaviors

For streaming operations, implement IStreamPipelineBehavior<TRequest, TResponse> and register manually:

public sealed class StreamLoggingBehavior<TRequest, TResponse>
    : IStreamPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<StreamLoggingBehavior<TRequest, TResponse>> _logger;

    public StreamLoggingBehavior(ILogger<StreamLoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async IAsyncEnumerable<TResponse> HandleAsync(
        TRequest request,
        StreamHandlerDelegate<TResponse> next,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var itemCount = 0;
        
        await foreach (var item in next(cancellationToken))
        {
            itemCount++;
            yield return item;
        }
        
        _logger.LogInformation(
            "Stream completed for {RequestName}: {ItemCount} items",
            typeof(TRequest).Name,
            itemCount);
    }
}

// Registration
builder.Services.AddModerator(config =>
{
    config.DiscoveredTypes = DiscoveredTypes.All;
    
    // Register stream behaviors manually
    config.Behaviors =
    [
        (typeof(IStreamPipelineBehavior<,>), typeof(StreamLoggingBehavior<,>)),
        (typeof(IStreamPipelineBehavior<,>), typeof(StreamValidationBehavior<,>))
    ];
});

🔧 Advanced Usage

Validation Pipeline

public sealed class ValidationBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken = default)
    {
        if (_validators.Any())
        {
            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())
            {
                throw new ValidationException(failures);
            }
        }
        
        return await next(cancellationToken);
    }
}

Transaction Pipeline

public sealed class TransactionBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : ITransactional
{
    private readonly IDbContext _dbContext;

    public TransactionBehavior(IDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<TResponse> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken = default)
    {
        using var transaction = await _dbContext.BeginTransactionAsync(cancellationToken);
        
        try
        {
            var response = await next(cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }
}

⚡ Source Generator

The optional Moderator.Generator package provides compile-time handler discovery and automatic registration:

What Gets Auto-Discovered

The source generator automatically discovers:

  • Request Handlers - IRequestHandler<TRequest, TResponse>
  • Notification Handlers - INotificationHandler<TNotification>
  • Stream Handlers - IStreamRequestHandler<TRequest, TResponse>

What Does NOT Get Auto-Discovered

  • Pipeline Behaviors - Must be manually registered to control execution order
  • Stream Pipeline Behaviors - Must be manually registered to control execution order

Why Behaviors Require Manual Registration

Pipeline behaviors execute in a specific order, and this order matters for correctness:

  • Logging should typically run first (outermost)
  • Validation should run before business logic
  • Transactions should wrap the handler execution (innermost)

Since the source generator cannot determine the correct execution order, behaviors must be explicitly registered in your desired order.

Automatic Registration

var builder = WebApplication.CreateBuilder(args);

// Register all discovered handlers and manually specify behaviors
builder.Services.AddModerator(config =>
{
    // Auto-register all discovered handlers
    config.DiscoveredTypes = DiscoveredTypes.All;
    
    // Manually register behaviors in execution order (outermost to innermost)
    config.Behaviors =
    [
        (typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)),      // Executes first
        (typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>)),  // Executes second
        (typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)),   // Executes third
        (typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>))   // Executes last
    ];
    
    // Stream behaviors also need manual registration if used
    config.StreamBehaviors = 
    [
        (typeof(IStreamPipelineBehavior<,>), typeof(StreamLoggingBehavior<,>)),
        (typeof(IStreamPipelineBehavior<,>), typeof(StreamValidationBehavior<,>))
    ];
});

var app = builder.Build();
app.Run();

Generated Discovery Code

The source generator creates a DiscoveredTypes class containing discovered handlers (but NOT behaviors):

// Auto-generated by Moderator.Generator
internal static partial class DiscoveredTypes
{
    public static IEnumerable<(Type ServiceType, Type ImplementationType)> All =
    [
        // Request Handlers
        (typeof(IRequestHandler<CreateNoteRequest, CreateNoteResponse>), 
         typeof(CreateNoteHandler)),
        
        // Notification Handlers (multiple handlers per notification)
        (typeof(INotificationHandler<NoteCreatedEvent>), 
         typeof(NoteCreatedEventHandler1)),
        (typeof(INotificationHandler<NoteCreatedEvent>), 
         typeof(NoteCreatedEventHandler2)),
        
        // Stream Handlers
        (typeof(IStreamRequestHandler<SearchNotesRequest, SearchResult>), 
         typeof(SearchNotesHandler))
        
        // Note: Pipeline behaviors are NOT included here
    ];
}

Type-Inferred Extension Methods

The generator also creates extension methods that eliminate the need to specify generic type parameters:

// Auto-generated extension methods
internal static class ModeratorExtensions
{
    public static Task<CreateNoteResponse> SendAsync(
        this IModerator moderator, 
        CreateNoteRequest request, 
        CancellationToken cancellationToken = default)
    {
        return moderator.SendAsync<CreateNoteRequest, CreateNoteResponse>(
            request, 
            cancellationToken);
    }
}

// Usage - no need to specify types!
var response = await moderator.SendAsync(new CreateNoteRequest(title));
// Instead of:
// var response = await moderator.SendAsync<CreateNoteRequest, CreateNoteResponse>(new CreateNoteRequest(title));

Benefits

  • Zero Runtime Reflection - All handlers discovered at compile time
  • AOT Compatible - Full Native AOT compilation support
  • Type Safety - Compile-time errors for missing handlers
  • Performance - No assembly scanning at startup
  • Cleaner Code - No need to specify generic type parameters
  • Auto-Registration - Single line to register all handlers

📊 Performance

Moderator is designed for high-performance scenarios:

  • Minimal allocations through struct usage where appropriate
  • Async/await throughout with ConfigureAwait(false)
  • Efficient service resolution caching
  • Zero reflection when using source generator

📦 Package Structure

The Moderator solution consists of three NuGet packages:

Package Purpose Target Framework
Moderator Core runtime implementation .NET Standard 2.1
Moderator.Contracts Public interfaces and contracts .NET Standard 2.1
Moderator.Generator Source generator for compile-time discovery .NET Standard 2.0

Package Versions

All packages are versioned together to ensure compatibility. Always use the same version across all Moderator packages in your project.

🤝 Contributing

We welcome contributions! Please see our Contributing Guidelines for details.

Development

# Clone the repository
git clone https://github.com/yourusername/moderator.git

# Build the solution
dotnet build

# Run tests
dotnet test

# Pack NuGet packages
dotnet pack

Reporting Issues

Found a bug or have a feature request? Please open an issue with:

  • Clear description of the problem
  • Steps to reproduce
  • Expected vs actual behavior
  • Environment details (.NET version, OS, etc.)

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.


Made with ❤️ by the Moderator contributors

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 Moderator.Contracts:

Package Downloads
Moderator

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.11.0 100 9/13/2025
0.10.0 173 9/11/2025
0.9.0 177 9/10/2025
0.8.0 159 9/10/2025
0.7.2 171 9/10/2025
0.7.0 173 9/10/2025
0.6.1-alpha 168 9/10/2025
0.6.0-alpha 162 9/10/2025
0.5.0-alpha 170 9/10/2025
0.4.0-alpha 165 9/10/2025
0.3.0-alpha 172 9/10/2025