AdaskoTheBeAsT.FluentValidation.MediatR 14.0.0

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

AdaskoTheBeAsT.FluentValidation.MediatR

Seamless FluentValidation integration for MediatR pipeline - automatic request validation before your handlers execute.

CodeFactor Build Status Azure DevOps tests Azure DevOps coverage Quality Gate Status Sonar Coverage Nuget Nuget


๐Ÿš€ Why This Library?

Stop writing validation code in every MediatR handler! This library automatically validates all your MediatR requests using FluentValidation before they reach your handlers.

Benefits:

  • โœ… Automatic validation - Set it up once, validate everywhere
  • โœ… Clean handlers - Keep business logic separate from validation
  • โœ… Fail fast - Invalid requests never reach your handlers
  • โœ… DRY principle - No boilerplate validation code in handlers
  • โœ… Streaming support - Works with both regular and streaming MediatR requests
  • โœ… Flexible - Single validator or multiple validators per request

๐Ÿ“ฆ Installation

dotnet add package AdaskoTheBeAsT.FluentValidation.MediatR

Supported frameworks:

  • .NET 10.0
  • .NET 9.0
  • .NET 8.0

Dependencies:

  • MediatR 14.2.0+
  • FluentValidation 12.1.0+

โšก Quick Start

Step 1: Create a Request and Validator

using FluentValidation;
using MediatR;

// Your MediatR request
public class CreateUserCommand : IRequest<UserResponse>
{
    public string Username { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
}

// Your FluentValidation validator
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Username)
            .NotEmpty()
            .MinimumLength(3)
            .MaximumLength(50);

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress();

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18)
            .WithMessage("User must be at least 18 years old");
    }
}

Step 2: Register with SimpleInjector

using AdaskoTheBeAsT.FluentValidation.MediatR;
using AdaskoTheBeAsT.FluentValidation.SimpleInjector;
using AdaskoTheBeAsT.MediatR.SimpleInjector;
using SimpleInjector;

var container = new Container();
var assemblies = new[] { typeof(Program).Assembly };

// Register FluentValidation validators
container.AddFluentValidation(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.AsScoped();
    cfg.RegisterAsSingleValidator(); // Default: one validator per request type
});

// Register MediatR with validation pipeline
container.AddMediatR(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.UsingBuiltinPipelineProcessorBehaviors(true);
    cfg.UsingPipelineProcessorBehaviors(typeof(FluentValidationPipelineBehavior<,>));
    cfg.UsingStreamPipelineBehaviors(typeof(FluentValidationStreamPipelineBehavior<,>));
});

Step 3: Use It!

var mediator = container.GetInstance<IMediator>();

var command = new CreateUserCommand
{
    Username = "john_doe",
    Email = "john@example.com",
    Age = 25
};

try
{
    // Validation happens automatically before handler execution
    var response = await mediator.Send(command);
    Console.WriteLine($"User created: {response.Id}");
}
catch (ValidationException ex)
{
    // Handle validation failures
    foreach (var error in ex.Errors)
    {
        Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}");
    }
}

๐Ÿ“– Detailed Usage

Use when: You have one validator per request type, or you want to combine multiple validators into one.

container.AddFluentValidation(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.AsScoped();
    cfg.RegisterAsSingleValidator(); // Default - can be omitted
});

container.AddMediatR(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.UsingBuiltinPipelineProcessorBehaviors(true);
    cfg.UsingPipelineProcessorBehaviors(typeof(FluentValidationPipelineBehavior<,>));
    cfg.UsingStreamPipelineBehaviors(typeof(FluentValidationStreamPipelineBehavior<,>));
});

Combining multiple validators:

If you need multiple validation rule sets, create a composite validator:

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        // Include rules from other validators
        Include(new UserNameValidationRules());
        Include(new EmailValidationRules());
        Include(new AgeValidationRules());
    }
}

// Mark sub-validators to prevent duplicate registration
[SkipValidatorRegistration]
public class UserNameValidationRules : AbstractValidator<CreateUserCommand>
{
    public UserNameValidationRules()
    {
        RuleFor(x => x.Username).NotEmpty().MinimumLength(3);
    }
}

[SkipValidatorRegistration]
public class EmailValidationRules : AbstractValidator<CreateUserCommand>
{
    public EmailValidationRules()
    {
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
    }
}

๐Ÿ“š Learn more about including rules


Approach 2: Multiple Validators (Collection)

Use when: You want to maintain separate, independent validators for the same request type.

container.AddFluentValidation(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.AsScoped();
    cfg.RegisterAsValidatorCollection(); // Register multiple validators
});

container.AddMediatR(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.UsingBuiltinPipelineProcessorBehaviors(true);
    cfg.UsingPipelineProcessorBehaviors(typeof(FluentValidationCollectionPipelineBehavior<,>));
    cfg.UsingStreamPipelineBehaviors(typeof(FluentValidationCollectionStreamPipelineBehavior<,>));
});

Example with multiple independent validators:

// Validator 1: Business rules
public class CreateUserBusinessRulesValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserBusinessRulesValidator()
    {
        RuleFor(x => x.Age).GreaterThanOrEqualTo(18);
    }
}

// Validator 2: Data format rules
public class CreateUserDataValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserDataValidator()
    {
        RuleFor(x => x.Username).NotEmpty().Matches("^[a-zA-Z0-9_]+$");
        RuleFor(x => x.Email).EmailAddress();
    }
}

// Both validators run, all errors from both are collected

โš ๏ธ Note: All validators run in parallel, and all validation errors from all validators are collected and thrown together.


Streaming Requests

Works seamlessly with MediatR streaming requests:

public class StreamDataQuery : IStreamRequest<DataChunk>
{
    public string Filter { get; set; }
    public int PageSize { get; set; }
}

public class StreamDataQueryValidator : AbstractValidator<StreamDataQuery>
{
    public StreamDataQueryValidator()
    {
        RuleFor(x => x.Filter).NotEmpty();
        RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
    }
}

// Validator runs BEFORE streaming starts
await foreach (var chunk in mediator.CreateStream(new StreamDataQuery { Filter = "test", PageSize = 50 }))
{
    Console.WriteLine(chunk);
}

๐Ÿ”ง Configuration Options

Validator Lifetime

container.AddFluentValidation(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
    cfg.AsScoped();        // Scoped (default, recommended)
    // cfg.AsSingleton();  // Singleton (if validators are stateless)
    // cfg.AsTransient();  // Transient (new instance each time)
});

Assembly Scanning

// Scan multiple assemblies
var assemblies = new[]
{
    typeof(Program).Assembly,
    typeof(CreateUserCommand).Assembly,
    typeof(OrderModule).Assembly
};

container.AddFluentValidation(cfg =>
{
    cfg.WithAssembliesToScan(assemblies);
});

๐ŸŽฏ When Validation Occurs

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. Client sends request                            โ”‚
โ”‚     mediator.Send(command)                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  2. MediatR Pipeline: Validation Behavior           โ”‚
โ”‚     โœ“ FluentValidationPipelineBehavior runs         โ”‚
โ”‚     โœ“ All validators execute                        โ”‚
โ”‚     โœ“ Errors collected                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚                   โ”‚
    Invalid             Valid
         โ”‚                   โ”‚
         โ–ผ                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ValidationEx-  โ”‚  โ”‚ 3. Your Handler Executes       โ”‚
โ”‚ ception thrown โ”‚  โ”‚    Handler<TRequest, TResponse>โ”‚
โ”‚                โ”‚  โ”‚    โœ“ Request is guaranteed     โ”‚
โ”‚ Handler never  โ”‚  โ”‚      to be valid               โ”‚
โ”‚ executes       โ”‚  โ”‚                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ› Troubleshooting

Validation Not Running

Problem: Validators are not executing.

Solutions:

  1. โœ… Ensure validators are in scanned assemblies
  2. โœ… Verify pipeline behavior is registered: UsingPipelineProcessorBehaviors(typeof(FluentValidationPipelineBehavior<,>))
  3. โœ… Check validator implements AbstractValidator<TRequest> or IValidator<TRequest>
  4. โœ… Ensure validator is not decorated with [SkipValidatorRegistration]

No Validators Found

// โŒ Wrong - assembly not included
cfg.WithAssembliesToScan(Array.Empty<Assembly>());

// โœ… Correct - include assemblies with validators
cfg.WithAssembliesToScan(new[] { typeof(CreateUserCommandValidator).Assembly });

ValidationException Not Caught

using FluentValidation; // โœ… Make sure to use this namespace

try
{
    await mediator.Send(command);
}
catch (ValidationException ex) // FluentValidation.ValidationException
{
    foreach (var error in ex.Errors)
    {
        Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}");
    }
}

Multiple Validators Only Showing Some Errors

If using collection approach, ensure you're using the Collection pipeline behaviors:

// โŒ Wrong - only uses first validator
cfg.UsingPipelineProcessorBehaviors(typeof(FluentValidationPipelineBehavior<,>));

// โœ… Correct - uses all validators
cfg.UsingPipelineProcessorBehaviors(typeof(FluentValidationCollectionPipelineBehavior<,>));

๐Ÿ—๏ธ Architecture

MediatR Request โ†’ Validation Pipeline Behavior โ†’ Handler
                         โ†“
                  IValidator<TRequest>
                         โ†“
                  Validation passes? 
                    โ†™         โ†˜
                  Yes          No
                   โ†“            โ†“
            Call next()    Throw ValidationException
                   โ†“
            Handler executes

This library works great with:


๐Ÿ“ Best Practices

  1. Use Single Validator approach when possible - simpler and more maintainable
  2. Fail fast - Put basic validation rules (NotEmpty, format checks) first
  3. Keep validators focused - One validator per request, or use Include() to compose
  4. Use meaningful error messages - Help your API consumers understand what went wrong
  5. Register validators as Scoped - Allows injecting scoped dependencies (e.g., DbContext)
  6. Test validators independently - Unit test validators separately from handlers
// Example: Testing validators
[Fact]
public void Should_Have_Error_When_Username_Is_Empty()
{
    var validator = new CreateUserCommandValidator();
    var command = new CreateUserCommand { Username = "" };
    
    var result = validator.Validate(command);
    
    Assert.False(result.IsValid);
    Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateUserCommand.Username));
}

๐Ÿงช Running Tests Locally

dotnet test

The test suite needs a MediatR license key. MediatR resolves it from the environment (MEDIATR_LICENSE_KEY, then the shared LUCKYPENNY_LICENSE_KEY) unless a key is passed explicitly via WithLicenseKey(...). In CI the value comes from the MEDIATR_LICENSE_KEY GitHub secret (see .github/workflows/ci.yml).

To supply it locally, copy the template and fill in your key:

cp .env.example .env
MEDIATR_LICENSE_KEY=your-license-key

.env is git ignored, so the key never lands in the repository. A module initializer in the test project (TestEnvironment) uses dotenv.net to probe upwards from the test output directory for the nearest .env and exports every KEY=VALUE entry into the test process before the first mediator is resolved. Variables that are already set in the environment are never overwritten, so CI values always win over the file.

This works the same for dotnet test and for the test runners in Visual Studio, Rider, and VS Code. If you prefer not to keep a file in the working tree, set a user-level environment variable instead (setx MEDIATR_LICENSE_KEY "your-license-key" on Windows) and restart your IDE or shell.


๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

๐Ÿ“„ License

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


๐Ÿ‘ค Author

Adam "AdaskoTheBeAsT" Pluciล„ski


โญ Show Your Support

If this library helps you, please give it a โญ on GitHub!

Product Compatible and additional computed target framework versions.
.NET 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 is compatible.  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. 
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
14.0.0 108 8/2/2026
13.0.0 455 11/12/2025
12.0.0 492 1/4/2025
11.2.0 448 8/18/2024
11.0.2 324 5/27/2024
11.0.1 397 1/27/2024
11.0.0 350 12/1/2023
10.2.0 381 7/16/2023
10.1.0 367 5/4/2023
10.0.0 447 2/17/2023
9.1.0 516 1/22/2023
9.0.0 602 11/13/2022
8.0.0 610 10/17/2022
7.2.1 664 9/25/2022
7.2.0 641 9/7/2022
7.1.0 668 7/5/2022
7.0.0 674 5/14/2022
6.0.0 559 1/11/2022
5.3.3 509 1/2/2022
5.3.2 604 10/30/2021
Loading failed

- MediatR 14.2.0
     - FluentValidation 12.1.1