Validator.NET — A Complete Guide for .NET DevelopersValidator.NET is a lightweight, extensible validation library for .NET designed to make validating objects, DTOs, and input models easy, consistent, and testable. This guide walks through why you might choose Validator.NET, how it works, common patterns, integrating it into ASP.NET Core, customization and extension points, performance and testing considerations, and a collection of practical examples you can adapt for your projects.
What is Validator.NET and when to use it
Validator.NET provides a fluent, declarative API for expressing validation rules separate from the models themselves. It fits well when you want:
- Separation of concerns — validation logic kept out of domain models or controllers.
- Reusability — reuse rule sets across endpoints and layers.
- Testability — validate rules independently in unit tests.
- Extensibility — add custom validators, rules, and localization.
- Integration — plug into ASP.NET Core model binding and middleware pipelines.
Use it for validating API requests, form input, background-job payloads, configuration objects, or anywhere consistent validation is needed.
Core concepts and API overview
- Validator: the central class that evaluates rules against an object instance and returns a result with errors.
- Rule: a single validation check (for example, NotEmpty, MinLength, Range).
- RuleSet: a named group of rules that can be executed together.
- Fluent builder: lets you chain rule definitions for properties in a clear readable style.
- ValidationResult: contains a collection of ValidationFailure items (property, error message, error code).
- Custom validators: implement an interface or inherit a base class to provide custom logic.
Basic usage pattern:
- Define a validator class for a model.
- Register rules in the constructor using fluent API.
- Run validation against an instance to get ValidationResult.
- Inspect or throw on failures as needed.
Simple example
using ValidatorDotNet; // hypothetical namespace public class UserDto { public string Email { get; set; } public string Password { get; set; } public int Age { get; set; } } public class UserDtoValidator : AbstractValidator<UserDto> { public UserDtoValidator() { RuleFor(x => x.Email).NotEmpty().EmailAddress(); RuleFor(x => x.Password).NotEmpty().MinimumLength(8); RuleFor(x => x.Age).GreaterThanOrEqualTo(18); } } // Usage var validator = new UserDtoValidator(); var result = validator.Validate(new UserDto { Email = "", Password = "123", Age = 16 }); if (!result.IsValid) { foreach (var error in result.Errors) { Console.WriteLine($"{error.PropertyName}: {error.Message}"); } }
Integrating Validator.NET with ASP.NET Core
Validator.NET can integrate with ASP.NET Core model binding to automatically execute validators during request processing. Typical steps:
- Add Validator.NET package and any integration package for ASP.NET Core.
- Register validators in DI (scoped/singleton depending on design).
- Configure MVC to use Validator.NET as a model validator provider or add a middleware/filter that runs validation.
Example registration in Startup.cs / Program.cs:
services.AddControllers() .AddValidatorNet(); // hypothetical extension method services.AddTransient<IValidator<UserDto>, UserDtoValidator>();
With integration, invalid models will produce 400 responses with a structured problem details body:
{ "errors": { "Email": ["Email must not be empty."], "Age": ["Age must be at least 18."] } }
You can customize the response shape by implementing an IValidationResponseFactory or writing an exception filter.
Rule types and common validators
Validator.NET typically provides:
- Presence checks: NotEmpty, NotNull
- String checks: MinimumLength, MaximumLength, Matches (regex), EmailAddress
- Numeric checks: GreaterThan, LessThanOrEqualTo, InclusiveBetween
- Collection checks: NotEmpty, Must (custom predicate for collection)
- Conditional rules: When(condition, rules) or RuleFor(x => x).Unless(…)
- Async validators: for database or external calls (e.g., checking uniqueness)
- Custom property-level and class-level validators
Examples:
- Conditional: RuleFor(x => x.Discount).GreaterThan(0).When(x => x.HasDiscount);
- Cross-property: Custom validator that ensures StartDate <= EndDate at class level.
Custom validators and localization
To create custom validation logic:
- Implement IPropertyValidator or inherit a base class (e.g., PropertyValidatorBase).
- Provide synchronous and optional asynchronous checks.
- Return error messages using placeholders for parameters.
Localize messages by supplying resource files (.resx) and configuring a message provider that pulls from those resources based on the current culture. Use message templates with placeholders:
”‘{PropertyName}’ must be greater than {ComparisonValue}.”
Best practices
- Keep validation logic in dedicated validator classes, not in controllers or domain entities.
- Prefer small focused rules and composition of RuleSets for different use-cases (create vs update).
- Use async validators sparingly; avoid database calls when possible by moving checks into services called after initial validation.
- Validate DTOs at the boundary, and map validated DTOs to domain models.
- Write unit tests for validators (positive and negative cases) and include edge cases.
Performance considerations
- Object graph traversal and reflection can add overhead. Cache compiled expressions when possible.
- Reuse validator instances (register as singleton if thread-safe) to avoid repeated allocations.
- Prefer synchronous checks unless you must access external resources.
- Benchmark with representative payload sizes (large nested DTOs, collections) to spot hotspots.
Testing validators
Unit tests should cover:
- Typical valid inputs (happy path).
- Invalid inputs for each rule.
- Conditional and cross-property rules.
- Custom validators and localized messages.
Example xUnit test:
[Fact] public void PasswordTooShort_ReturnsError() { var validator = new UserDtoValidator(); var result = validator.Validate(new UserDto { Password = "123" }); Assert.False(result.IsValid); Assert.Contains(result.Errors, e => e.PropertyName == "Password" && e.ErrorMessage.Contains("minimum")); }
Advanced topics
- Composition: Combine smaller validators for nested objects.
- RuleSets: Execute different rule groups for Create vs Update operations.
- Pipeline integration: Use validation middleware in background job processing.
- Auto-discovery: Scan assemblies and register validators automatically in DI.
- Extending for GraphQL or gRPC—adapt model binding and error mapping.
Common pitfalls
- Putting validation logic in domain entities — leads to coupling and harder testing.
- Overusing async DB calls in validators — slows request processing.
- Forgetting to validate nested objects/collections.
- Not separating rules by operation (create/update), causing inappropriate failures.
Migration tips (from FluentValidation or other libraries)
- Map common rule names directly (NotEmpty, EmailAddress, Length).
- Reimplement any custom validators you’ll need and preserve message templates.
- Swap integration points (MVC filter/validator provider) while keeping DTOs unchanged.
- Run tests to ensure behavior parity, especially around conditional rules and RuleSets.
Example: complex validator with RuleSets and async check
public class AccountDto { public string Username { get; set; } public string Email { get; set; } public string Password { get; set; } } public class AccountDtoValidator : AbstractValidator<AccountDto> { public AccountDtoValidator(IUserRepository userRepository) { RuleSet("Create", () => { RuleFor(x => x.Username) .NotEmpty() .MustAsync(async (username, ct) => !await userRepository.ExistsAsync(username)) .WithMessage("Username is already taken."); RuleFor(x => x.Password).NotEmpty().MinimumLength(8); }); RuleSet("Update", () => { RuleFor(x => x.Username).NotEmpty(); // Don't check uniqueness on update unless username changed }); RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrEmpty(x.Email)); } }
Summary
Validator.NET is a practical choice when you need a clear, testable, and extensible validation system for .NET applications. It keeps validation concerns out of controllers and domain models, supports fluent rule definitions, integrates with ASP.NET Core, and is flexible enough to handle simple to advanced scenarios like async checks and rule sets. Apply best practices—separate concerns, write unit tests, limit expensive operations—and you’ll have predictable, maintainable validation across your application.
Leave a Reply