Skip to content

Lightweight, expressive validation library for C#, with a clean rule-based DSL and zero dependencies.

License

Notifications You must be signed in to change notification settings

akikari/Fox.ValidationKit

Repository files navigation

🎯 Fox.ValidationKit

.NET Build and Test NuGet NuGet Downloads License: MIT codecov

Lightweight .NET validation library with fluent API and zero dependencies

Fox.ValidationKit is a lightweight, expressive validation library for .NET with a fluent API. It provides strongly-typed validation rules with support for synchronous and asynchronous validation, custom rules, and optional ResultKit integration.

📋 Table of Contents

🤔 Why Fox.ValidationKit?

Traditional approach:

// ❌ Manual validation with boilerplate code
public class UserService
{
    public void RegisterUser(User user)
    {
        if (user == null) throw new ArgumentNullException(nameof(user));
        if (string.IsNullOrEmpty(user.Email)) throw new ArgumentException("Email is required");
        if (user.Age < 18) throw new ArgumentException("Must be 18 or older");
        if (user.Age > 150) throw new ArgumentException("Age is unrealistic");
        // ... more validation logic
    }
}

Fox.ValidationKit approach:

// ✅ Clean, fluent validation with reusable validator
public class UserValidator : Validator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty().MinLength(2).MaxLength(50);
        RuleFor(x => x.LastName).NotEmpty().MinLength(2).MaxLength(50);
        RuleFor(x => x.Email).NotEmpty().Matches(@"^[^@]+@[^@]+\.[^@]+$");
        RuleFor(x => x.Age).GreaterThan(0).LessThan(150).Custom((user, age) => 
            age >= 18, "User must be at least 18 years old");
    }
}

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

if (!result.IsValid)
{
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"{error.PropertyName}: {error.Message}");
    }
}

✨ Features

  • Fluent API - Intuitive, type-safe validation rule configuration
  • Zero Dependencies - No external dependencies, only .NET BCL
  • Strongly Typed - Expression-based property selection with IntelliSense support
  • Rich Rule Set - 15+ built-in validation rules (NotNull, NotEmpty, GreaterThan, LessThan, Between, Matches, etc.)
  • Custom Rules - Support for synchronous and asynchronous custom validation logic
  • Error Codes - FVK### error codes for localization and structured error handling
  • Async Support - ValidateAsync for asynchronous validation scenarios
  • Collection Validation - Validate collections with NotEmpty, MinCount, MaxCount, RuleForEach
  • Conditional Validation - Apply rules conditionally with When and Unless
  • Cascade Modes - Continue or stop validation on first failure
  • Nested Validation - Validate complex objects with SetValidator
  • ResultKit Integration - Optional integration with Fox.ResultKit for Railway Oriented Programming
  • Localization Ready - IValidationMessageProvider for custom error messages
  • Multi-Targeting - Supports .NET 8.0, .NET 9.0, and .NET 10.0

📦 Installation

dotnet add package Fox.ValidationKit

NuGet Package Manager:

Install-Package Fox.ValidationKit

PackageReference:

<PackageReference Include="Fox.ValidationKit" Version="1.0.0" />

Optional: ResultKit Integration

For Railway Oriented Programming support:

dotnet add package Fox.ValidationKit.ResultKit

🚀 Quick Start

1. Define Your Model

public sealed class User
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Email { get; set; }
    public int Age { get; set; }
    public string? PhoneNumber { get; set; }
}

2. Create a Validator

public sealed class UserValidator : Validator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty("First name is required")
            .MinLength(2)
            .MaxLength(50);

        RuleFor(x => x.LastName)
            .NotEmpty("Last name is required")
            .MinLength(2)
            .MaxLength(50);

        RuleFor(x => x.Email)
            .NotEmpty("Email is required")
            .Matches(@"^[^@]+@[^@]+\.[^@]+$", "Email format is invalid");

        RuleFor(x => x.Age)
            .GreaterThan(0, "Age must be positive")
            .LessThan(150, "Age must be realistic")
            .Custom((user, age) => age >= 18, "User must be at least 18 years old");

        RuleFor(x => x.PhoneNumber)
            .Matches(@"^\+?[\d\s\-\(\)]+$", "Phone number contains invalid characters");
    }
}

3. Validate Your Data

var validator = new UserValidator();
var user = new User
{
    FirstName = "John",
    LastName = "Doe",
    Email = "john.doe@example.com",
    Age = 25,
    PhoneNumber = "+1-555-123-4567"
};

var result = validator.Validate(user);

if (result.IsValid)
{
    Console.WriteLine("✓ User is valid!");
}
else
{
    Console.WriteLine("✗ Validation failed:");
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"  - {error.PropertyName}: {error.Message}");
        if (error.ErrorCode != null)
        {
            Console.WriteLine($"    Error Code: {error.ErrorCode}");
        }
    }
}

📚 Validation Rules

Basic Validation

Method Description Error Code
NotNull(message) Ensures value is not null FVK001
NotEmpty(message) Ensures string is not null, empty, or whitespace FVK002

Example:

RuleFor(x => x.FirstName)
    .NotEmpty("First name is required");

RuleFor(x => x.Address)
    .NotNull("Address is required");

Comparison Validation

Method Description Error Code
GreaterThan(value, message) Ensures value is greater than specified value FVK202
LessThan(value, message) Ensures value is less than specified value FVK203
Between(min, max, message) Ensures value is between min and max (inclusive) FVK204
Equal(value, message) Ensures value equals another property value FVK200
NotEqual(value, message) Ensures value does not equal another property value FVK201

Example:

RuleFor(x => x.Age)
    .GreaterThan(0, "Age must be positive")
    .LessThan(150, "Age must be realistic")
    .Between(18, 65, "Age must be between 18 and 65");

RuleFor(x => x.Price)
    .GreaterThan(0.01m);

RuleFor(x => x.Password)
    .NotEqual(u => u.Username, "Password must be different from username");

String Validation

Method Description Error Code
MinLength(length, message) Ensures string has minimum length FVK100
MaxLength(length, message) Ensures string has maximum length FVK101
Length(min, max, message) Ensures string length is within range FVK102
Matches(regex, message) Ensures string matches regex pattern FVK304
EmailAddress(message) Validates email address format FVK301
Url(message) Validates URL format (HTTP/HTTPS) FVK302
CreditCard(message) Validates credit card number (Luhn algorithm) FVK303

Example:

RuleFor(x => x.Username)
    .NotEmpty()
    .MinLength(3)
    .MaxLength(20)
    .Matches(@"^[a-zA-Z0-9_]+$", "Username can only contain letters, numbers, and underscores");

RuleFor(x => x.Email)
    .EmailAddress("Invalid email address");

RuleFor(x => x.Website)
    .Url("Invalid URL format");

RuleFor(x => x.CardNumber)
    .CreditCard("Invalid credit card number");

Collection Validation

Method Description Error Code
NotEmpty(message) Ensures collection is not null or empty FVK400
MinCount(count, message) Ensures collection has minimum count FVK401
MaxCount(count, message) Ensures collection has maximum count FVK402
RuleForEach(predicate, message) Validates each element in collection -

Example:

RuleFor(x => x.Tags)
    .NotEmpty("At least one tag is required")
    .MinCount(1)
    .MaxCount(10)
    .RuleForEach(tag => tag.Length >= 2, "Each tag must be at least 2 characters");

RuleFor(x => x.Addresses)
    .NotEmpty("At least one address is required")
    .MaxCount(5, "Maximum 5 addresses allowed");

Enum Validation

Method Description Error Code
IsInEnum(message) Ensures value is a valid enum value FVK500

Example:

public enum UserRole
{
    Admin,
    User,
    Guest
}

RuleFor(x => x.Role)
    .IsInEnum("Invalid user role");

Custom Validation

Method Description Error Code
Custom(predicate, message) Custom synchronous validation FVK900
CustomAsync(predicate, message) Custom asynchronous validation FVK900

Example:

RuleFor(x => x.Age)
    .Custom((user, age) => age >= 18, "User must be at least 18 years old");

RuleFor(x => x.Email)
    .CustomAsync(async (user, email, ct) =>
    {
        var exists = await emailService.ExistsAsync(email, ct);
        return !exists;
    }, "Email is already registered");

🔥 Advanced Scenarios

Conditional Validation

Apply validation rules conditionally:

public sealed class OrderValidator : Validator<Order>
{
    public OrderValidator()
    {
        RuleFor(x => x.ShippingAddress)
            .NotNull()
            .When(order => order.RequiresShipping);

        RuleFor(x => x.PickupLocation)
            .NotEmpty()
            .Unless(order => order.RequiresShipping);
    }
}

Cascade Modes

Control validation behavior when rules fail:

public sealed class UserValidator : Validator<User>
{
    public UserValidator()
    {
        // Stop validation on first failure for this property
        RuleFor(x => x.Email)
            .Cascade(CascadeMode.Stop)
            .NotEmpty("Email is required")
            .EmailAddress("Invalid email format")
            .CustomAsync(async (user, email, ct) =>
            {
                return await emailService.IsUniqueAsync(email, ct);
            }, "Email is already registered");

        // Continue validation even if rules fail (default)
        RuleFor(x => x.Password)
            .Cascade(CascadeMode.Continue)
            .NotEmpty()
            .MinLength(8)
            .Matches(@"[A-Z]", "Password must contain uppercase letter")
            .Matches(@"[a-z]", "Password must contain lowercase letter")
            .Matches(@"\d", "Password must contain digit");
    }
}

Nested Object Validation

Validate complex objects with nested validators:

public sealed class AddressValidator : Validator<Address>
{
    public AddressValidator()
    {
        RuleFor(x => x.Street).NotEmpty();
        RuleFor(x => x.City).NotEmpty();
        RuleFor(x => x.ZipCode).Matches(@"^\d{5}$");
    }
}

public sealed class UserValidator : Validator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty();
        RuleFor(x => x.LastName).NotEmpty();

        // Validate nested object
        RuleFor(x => x.Address).SetValidator(new AddressValidator());
    }
}

Asynchronous Validation

Support for async validation:

public sealed class UserValidator : Validator<User>
{
    private readonly IUserRepository repository;

    public UserValidator(IUserRepository repository)
    {
        this.repository = repository;

        RuleFor(x => x.Email)
            .NotEmpty()
            .CustomAsync(async (user, email, cancellationToken) =>
            {
                var exists = await repository.EmailExistsAsync(email, cancellationToken);
                return !exists;
            }, "Email is already registered");
    }
}

// Usage
var result = await validator.ValidateAsync(user, cancellationToken);

🎨 ResultKit Integration

Fox.ValidationKit.ResultKit provides seamless integration with Fox.ResultKit for Railway Oriented Programming:

Installation

dotnet add package Fox.ValidationKit.ResultKit

ValidateAsResult

Convert ValidationResult to Result:

using Fox.ValidationKit.ResultKit;

var validator = new UserValidator();
var result = validator.ValidateAsResult(user);

return result.Match(
    onSuccess: () => Ok("User validated successfully"),
    onFailure: error => BadRequest(error)
);

ValidateAsResultValue

Return the validated value with Result<T>:

var result = validator.ValidateAsResultValue(user);

return result.Match(
    onSuccess: validUser => Ok(validUser),
    onFailure: error => BadRequest(error)
);

ValidateAsErrorsResult

Get individual validation errors as separate Results:

var errorsResult = validator.ValidateAsErrorsResult(user);

if (!errorsResult.IsSuccess)
{
    foreach (var error in errorsResult.Errors)
    {
        Console.WriteLine($"- {error.Error}");
    }
}

Async Variants

All methods have async versions:

var result = await validator.ValidateAsResultAsync(user, cancellationToken);
var resultValue = await validator.ValidateAsResultValueAsync(user, cancellationToken);
var errorsResult = await validator.ValidateAsErrorsResultAsync(user, cancellationToken);

🏷️ Error Codes

Fox.ValidationKit provides structured error codes for all built-in validation rules:

Category Error Codes Description
Null/Empty FVK001-003 NotNull, NotEmpty, CollectionNotEmpty
Length FVK100-102 MinLength, MaxLength, Length
Comparison FVK200-204 Equal, NotEqual, GreaterThan, LessThan, Between
Pattern FVK301-304 EmailAddress, Url, CreditCard, Matches
Collection FVK400-402 NotEmpty, MinCount, MaxCount
Enum FVK500 IsInEnum
Custom FVK900 Custom validation rules

Localization with Message Provider

Implement IValidationMessageProvider for custom error messages:

public class LocalizedMessageProvider : IValidationMessageProvider
{
    public string GetMessage(string errorCode, string propertyName, params object[] args)
    {
        return errorCode switch
        {
            ValidationErrorCodes.NotEmpty => $"{propertyName} nem lehet üres",
            ValidationErrorCodes.MinLength => $"{propertyName} legalább {args[0]} karakter kell legyen",
            ValidationErrorCodes.EmailAddress => $"{propertyName} nem érvényes e-mail cím",
            _ => $"{propertyName} érvénytelen"
        };
    }
}

// Usage
var validator = new UserValidator();
validator.UseMessageProvider(new LocalizedMessageProvider());

💡 Design Principles

Fox.ValidationKit follows these design principles:

  • Explicit Validation - All validation rules are explicitly declared using a fluent API
  • Type Safety First - Leverage C#'s strong type system (nullable reference types, expression trees, generic constraints)
  • Zero External Dependencies - No external dependencies, relies only on .NET BCL
  • Developer Experience - Clear error messages, IntelliSense-friendly APIs, comprehensive XML documentation
  • Flexible - Support for sync, async, custom validation rules, conditional validation, nested objects

📋 Requirements

  • .NET 8.0, .NET 9.0, or .NET 10.0
  • C# 12.0 or later (for modern language features)

🤝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Fox.ValidationKit is intentionally lightweight and feature-focused. The goal is to remain a simple, zero-dependency validation library with a fluent API.

What We Welcome

  • Bug fixes - Issues with existing validation rules or functionality
  • Documentation improvements - Clarifications, examples, typo fixes
  • Performance optimizations - Without breaking API compatibility
  • New validation rules - If they are commonly needed and align with the library's scope

What We Generally Do Not Accept

  • ❌ New dependencies or third-party packages (zero-dependency policy)
  • ❌ Large feature additions that increase complexity
  • ❌ Breaking API changes

If you want to propose a significant change, please open an issue first to discuss whether it aligns with the project's philosophy.

Build Policy

The project enforces a strict build policy to ensure code quality:

  • No errors allowed - Build must be error-free
  • No warnings allowed - All compiler warnings must be resolved
  • No messages allowed - Informational messages must be suppressed or addressed

All pull requests must pass this requirement.

Code Quality Standards

Fox.ValidationKit follows strict coding standards:

  • Comprehensive unit tests required (xUnit + FluentAssertions)
  • Maximum test coverage required - Aim for 100% line and branch coverage. Tests may only be omitted if they would introduce artificial complexity (e.g., testing unreachable code paths, framework internals, or compiler-generated code). Use [ExcludeFromCodeCoverage] sparingly and only for justified cases.
  • XML documentation for all public APIs - Clear, concise documentation with examples
  • Follow Microsoft coding conventions - See .github/copilot-instructions.md for project-specific style
  • Zero warnings, zero errors build policy - Strict enforcement

Code Style

  • Follow the existing code style (see .github/copilot-instructions.md)
  • Use file-scoped namespaces
  • Enable nullable reference types
  • Use expression-bodied members for simple properties/methods
  • Private fields: camelCase without underscore prefix
  • Add XML documentation decorators (98-character width)

How to Contribute

  1. Fork the repository
  2. Create a feature branch from main
  3. Follow the coding standards in .github/copilot-instructions.md
  4. Write comprehensive unit tests (aim for 100% coverage)
  5. Ensure all tests pass and build is clean (zero warnings/errors)
  6. Submit a pull request

📄 License

Fox.ValidationKit is licensed under the MIT License.

Copyright (c) 2026 Károly Akácz


👤 Author

Károly Akácz


About

Lightweight, expressive validation library for C#, with a clean rule-based DSL and zero dependencies.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Languages