ResultFlow.FluentValidation 2.2.0

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

ResultFlow.FluentValidation

FluentValidation integration for ResultFlow.

This package provides seamless integration between FluentValidation and ResultFlow, allowing you to convert validation results into Result<T> objects with comprehensive error handling.

๐Ÿ“ฆ Installation

Install the NuGet package:

dotnet add package ResultFlow.FluentValidation --version 1.0.1-beta

Or via Package Manager:

Install-Package ResultFlow.FluentValidation -Version 1.0.1-beta

๐Ÿš€ Quick Start

using FluentValidation;
using ResultFlow.Results;
using ResultFlow.FluentValidation.Extensions;

// Define your validator
public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MinimumLength(2).WithMessage("Name must be at least 2 characters");

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Email must be valid");

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18).WithMessage("User must be 18 or older");
    }
}

// Use the validator with ResultFlow
public class UserService
{
    private readonly UserValidator _validator;

    public async Task<Result<User>> CreateUserAsync(User user)
    {
        // Validate and convert to Result in one call
        return await _validator.ValidateAsync(user);
    }
}

โœจ Features

  • โœ… Seamless Integration - Convert FluentValidation results directly to Result<T>
  • โœ… Comprehensive Error Details - Capture all validation errors with property-level grouping
  • โœ… Async Support - Full async/await validation support
  • โœ… Type Safe - Strongly typed validation with generic support
  • โœ… Rich Metadata - Validation errors stored as metadata for API responses
  • โœ… Zero Configuration - Works out of the box with existing validators

๐Ÿ”„ How It Works

The FluentValidation integration provides two extension methods:

1. ToResult<T> - Convert ValidationResult

public static Result<T> ToResult<T>(
    this ValidationResult validationResult, 
    T? value = default)

Converts a FluentValidation ValidationResult to a Result<T>:

  • On Success โœ… Returns Result<T>.Ok(value)
  • On Failure โŒ Returns Result<T>.Failed(error) with validation errors grouped by property

2. ValidateAsync<T> - Validate and Convert

public static async Task<Result<T>> ValidateAsync<T>(
    this IValidator<T> validator, 
    T instance)

Validates an object and returns a Result<T> in a single call.

๐Ÿ“– Usage Examples

Basic Validation

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required");

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Must be a valid email address");

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18).WithMessage("Must be 18 or older");
    }
}

// Usage
var validator = new UserValidator();
var result = await validator.ValidateAsync(user);

if (result.IsOk)
{
    // User is valid, use result.Value
    Console.WriteLine($"User {result.Value.Name} is valid");
}
else
{
    // Validation failed, errors are in result.Error
    Console.WriteLine($"Validation failed: {result.Error.Message}");
    Console.WriteLine($"Details: {result.Error.Details}");
}

Validation in Controllers

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    private readonly UserValidator _validator;

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserRequest request)
    {
        // Validate request
        var validationResult = await _validator.ValidateAsync(request);
        
        // Convert to Result and check
        if (!validationResult.IsOk)
            return validationResult.ToActionResult();

        // Process valid request
        var user = new User 
        { 
            Name = request.Name, 
            Email = request.Email 
        };
        
        var result = await _userService.CreateUserAsync(user);
        return result.ToActionResult();
    }
}

Validation in Services

public class UserService : IUserService
{
    private readonly UserValidator _validator;
    private readonly IUserRepository _repository;

    public async Task<Result<User>> CreateUserAsync(User user)
    {
        // Validate the user
        var validationResult = await _validator.ValidateAsync(user);
        
        if (!validationResult.IsOk)
            return validationResult;

        // Proceed with creation
        try
        {
            var createdUser = await _repository.AddAsync(user);
            return Result<User>.Ok(createdUser);
        }
        catch (Exception ex)
        {
            return Result<User>.Failed(
                InternalServerError.FromException(ex)
            );
        }
    }
}

Complex Validation Rules

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Product name is required")
            .Length(2, 100).WithMessage("Product name must be between 2 and 100 characters");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than zero")
            .LessThanOrEqualTo(999999).WithMessage("Price cannot exceed 999,999");

        RuleFor(x => x.Description)
            .NotEmpty().When(x => x.Price > 100)
            .WithMessage("Description is required for products over 100");

        RuleFor(x => x.Category)
            .NotEmpty().WithMessage("Category is required")
            .Must(ValidateCategory).WithMessage("Invalid category");

        RuleForEach(x => x.Tags)
            .NotEmpty().WithMessage("Tags cannot be empty");
    }

    private bool ValidateCategory(string category)
        => !string.IsNullOrEmpty(category) && category.Length <= 50;
}

// Usage
var validator = new ProductValidator();
var result = await validator.ValidateAsync(product);

result.Match(
    onSuccess: validProduct => Console.WriteLine($"Product {validProduct.Name} is valid"),
    onFailure: error => Console.WriteLine($"Validation error: {error.Message}")
);

Chaining with Other Operations

public class OrderService
{
    private readonly OrderValidator _validator;
    private readonly IOrderRepository _repository;

    public async Task<Result<Order>> CreateOrderAsync(Order order)
    {
        return await _validator.ValidateAsync(order)
            .Then(validOrder => ValidateInventoryAsync(validOrder))
            .Then(validOrder => ValidatePricingAsync(validOrder))
            .Then(validOrder => SaveOrderAsync(validOrder));
    }

    private async Task<Result<Order>> ValidateInventoryAsync(Order order)
    {
        foreach (var item in order.Items)
        {
            var stock = await _repository.GetStockAsync(item.ProductId);
            if (stock < item.Quantity)
                return Result<Order>.Failed(
                    ValidationError.WithDefaults(
                        "Insufficient inventory",
                        details: $"Product {item.ProductId} has only {stock} units available"
                    )
                );
        }
        
        return Result<Order>.Ok(order);
    }

    private async Task<Result<Order>> ValidatePricingAsync(Order order)
    {
        // Additional pricing validation
        return Result<Order>.Ok(order);
    }

    private async Task<Result<Order>> SaveOrderAsync(Order order)
    {
        try
        {
            var saved = await _repository.AddAsync(order);
            return Result<Order>.Ok(saved);
        }
        catch (Exception ex)
        {
            return Result<Order>.Failed(
                InternalServerError.FromException(ex)
            );
        }
    }
}

๐Ÿ“‹ Error Response Format

When validation fails, the error response includes detailed information:

{
  "code": "VALIDATION_FAILED",
  "message": "Validation failed",
  "details": "{\"Name\":[\"Name is required\"],\"Email\":[\"Email is required\",\"Must be a valid email address\"],\"Age\":[\"Must be 18 or older\"]}",
  "metadata": {
    "errors": {
      "Name": ["Name is required"],
      "Email": ["Email is required", "Must be a valid email address"],
      "Age": ["Must be 18 or older"]
    }
  }
}

๐ŸŒ Real-World Example: Complete Service

using FluentValidation;
using ResultFlow.Results;
using ResultFlow.FluentValidation.Extensions;

// Domain Model
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Description { get; set; }
    public string Category { get; set; }
    public List<string> Tags { get; set; } = new();
}

// Validator
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Product name is required")
            .Length(3, 100).WithMessage("Product name must be between 3 and 100 characters");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than zero")
            .LessThanOrEqualTo(999999).WithMessage("Price cannot exceed 999,999");

        RuleFor(x => x.Description)
            .NotEmpty().WithMessage("Description is required")
            .MinimumLength(10).WithMessage("Description must be at least 10 characters");

        RuleFor(x => x.Category)
            .NotEmpty().WithMessage("Category is required")
            .Length(2, 50).WithMessage("Category must be between 2 and 50 characters");

        RuleForEach(x => x.Tags)
            .NotEmpty().WithMessage("Tags cannot be empty")
            .Length(2, 20).WithMessage("Each tag must be between 2 and 20 characters");
    }
}

// Repository Interface
public interface IProductRepository
{
    Task<Product> AddAsync(Product product);
    Task<Product> UpdateAsync(Product product);
    Task<Product> GetByIdAsync(int id);
    Task DeleteAsync(int id);
}

// Service
public class ProductService : IProductService
{
    private readonly ProductValidator _validator;
    private readonly IProductRepository _repository;
    private readonly ILogger<ProductService> _logger;

    public ProductService(
        ProductValidator validator,
        IProductRepository repository,
        ILogger<ProductService> logger)
    {
        _validator = validator;
        _repository = repository;
        _logger = logger;
    }

    public async Task<Result<Product>> CreateProductAsync(Product product)
    {
        _logger.LogInformation("Creating product: {ProductName}", product.Name);

        return await _validator.ValidateAsync(product)
            .Bind(validProduct => SaveProductAsync(validProduct))
            .Tap(savedProduct => _logger.LogInformation(
                "Product created successfully: {ProductId}", savedProduct.Id))
            .TapError(error => _logger.LogError(
                "Failed to create product: {ErrorMessage}", error.Message));
    }

    public async Task<Result<Product>> UpdateProductAsync(int id, Product product)
    {
        _logger.LogInformation("Updating product: {ProductId}", id);

        var existingProduct = await _repository.GetByIdAsync(id);
        if (existingProduct == null)
            return Result<Product>.Failed(
                NotFoundError.ByIdentifier("Product", id)
            );

        product.Id = id;

        return await _validator.ValidateAsync(product)
            .Bind(validProduct => UpdateProductInDatabaseAsync(validProduct))
            .Tap(updatedProduct => _logger.LogInformation(
                "Product updated successfully: {ProductId}", updatedProduct.Id))
            .TapError(error => _logger.LogError(
                "Failed to update product: {ErrorMessage}", error.Message));
    }

    private async Task<Result<Product>> SaveProductAsync(Product product)
    {
        try
        {
            var savedProduct = await _repository.AddAsync(product);
            return Result<Product>.Ok(savedProduct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Database error while creating product");
            return Result<Product>.Failed(
                InternalServerError.ForOperation("CreateProduct", ex)
            );
        }
    }

    private async Task<Result<Product>> UpdateProductInDatabaseAsync(Product product)
    {
        try
        {
            var updatedProduct = await _repository.UpdateAsync(product);
            return Result<Product>.Ok(updatedProduct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Database error while updating product");
            return Result<Product>.Failed(
                InternalServerError.ForOperation("UpdateProduct", ex)
            );
        }
    }
}

// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductService _service;

    public ProductsController(ProductService service) => _service = service;

    [HttpPost]
    [ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status422UnprocessableEntity)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
    public async Task<IActionResult> CreateProduct(CreateProductRequest request)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            Description = request.Description,
            Category = request.Category,
            Tags = request.Tags
        };

        var result = await _service.CreateProductAsync(product);
        return result.ToActionResult();
    }

    [HttpPut("{id}")]
    [ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status422UnprocessableEntity)]
    [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
    public async Task<IActionResult> UpdateProduct(int id, UpdateProductRequest request)
    {
        var product = new Product
        {
            Id = id,
            Name = request.Name,
            Price = request.Price,
            Description = request.Description,
            Category = request.Category,
            Tags = request.Tags
        };

        var result = await _service.UpdateProductAsync(id, product);
        return result.ToActionResult();
    }
}

๐Ÿ”— Integration with ASP.NET Core

When combined with ResultFlow.AspNetCore, validation errors automatically map to HTTP 422 responses:

[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
    var user = new User { Name = request.Name, Email = request.Email };
    var result = await _userValidator.ValidateAsync(user);
    
    // If validation fails, automatically returns 422 with detailed error info
    return result.ToActionResult();
}

๐Ÿ“š API Reference

ToResult<T>

/// <summary>
/// Converts FluentValidation results to Result{T}
/// </summary>
/// <typeparam name="T">The type of the value being validated.</typeparam>
/// <param name="validationResult">The FluentValidation result to convert.</param>
/// <param name="value">The value associated with the validation result.</param>
/// <returns>A successful Result{T} if validation passed, otherwise a failed Result{T}.</returns>
public static Result<T> ToResult<T>(
    this ValidationResult validationResult, 
    T? value = default)

ValidateAsync<T>

/// <summary>
/// Validates an object and returns Result{T}
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="validator">The FluentValidation validator to use.</param>
/// <param name="instance">The instance to validate.</param>
/// <returns>A successful Result{T} if validation passed, otherwise a failed Result{T}.</returns>
public static async Task<Result<T>> ValidateAsync<T>(
    this IValidator<T> validator, 
    T instance)

๐Ÿงช Testing with Validation

[TestClass]
public class ProductValidatorTests
{
    private ProductValidator _validator;

    [TestInitialize]
    public void Setup() => _validator = new ProductValidator();

    [TestMethod]
    public async Task CreateProduct_WithValidData_ReturnsSuccess()
    {
        // Arrange
        var product = new Product
        {
            Name = "Test Product",
            Price = 99.99m,
            Description = "This is a test product",
            Category = "Electronics",
            Tags = new List<string> { "test", "product" }
        };

        // Act
        var result = await _validator.ValidateAsync(product);

        // Assert
        Assert.IsTrue(result.IsOk);
        Assert.AreEqual(product, result.Value);
    }

    [TestMethod]
    public async Task CreateProduct_WithInvalidPrice_ReturnsFailed()
    {
        // Arrange
        var product = new Product
        {
            Name = "Test Product",
            Price = 0,  // Invalid
            Description = "This is a test product",
            Category = "Electronics",
            Tags = new List<string> { "test", "product" }
        };

        // Act
        var result = await _validator.ValidateAsync(product);

        // Assert
        Assert.IsFalse(result.IsOk);
        Assert.IsTrue(result.Error.Message.Contains("Validation failed"));
    }
}

๐Ÿค Contributing

Contributions are welcome! You can:

  • ๐Ÿž Report bugs by creating an Issue
  • ๐Ÿ’ก Suggest features in Discussions
  • ๐Ÿ”ง Submit Pull Requests with improvements

๐Ÿ’ฌ Support

Need help?

๐Ÿ“Š Workflow Example

User Input
    โ†“
Validator (FluentValidation)
    โ†“
ValidateAsync() Extension
    โ†“
Result<T> (Success or Failure)
    โ†“
Service Logic (if validation passed)
    โ†“
ToActionResult() Extension (if using ASP.NET Core)
    โ†“
HTTP Response (200, 400, 422, 500, etc.)

Made with โค๏ธ by said1993

Updated: 2025-11-23 19:08:17 UTC

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
2.2.0 457 12/8/2025
2.0.2 194 11/25/2025
2.0.0 194 11/25/2025