Validot
Validot is a performance-first, compact library for advanced model validation. Using a simple declarative fluent interface, it efficiently handles classes, structs, nested members, collections, nullables, plus any relation or combination of them. It also supports translations, custom logic extensions with tests, and DI containers.
Install / Use
/learn @bartoszlenar/ValidotREADME
Announcement! Validot is archived!
Validot has been my public pet project since 2020, a proof-of-concept turned into a standalone, fully-featured NuGet library. Its core focus is on performance and a low-allocation approach. On release day, Validot was 2.5 times faster while consuming 8 times less memory than the industry's gold standard: FluentValidation! I pushed dotnet memory performance and optimizations to their limits. And I'm proud of my work.
Given my daily responsibilities and other coding projects, I have to confess that it seems improbable I'll have time to keep working on Validot.
I appreciate all the contributors, and especially, I extend my gratitude to Jeremy Skinner for his work on FluentValidation. I genuinely believe I played a role in the open source community by motivating Jeremy to enhance FluentValidation's performance.
Quickstart
Add the Validot nuget package to your project using dotnet CLI:
dotnet add package Validot
All the features are accessible after referencing single namespace:
using Validot;
And you're good to go! At first, create a specification for your model with the fluent api.
Specification<UserModel> specification = _ => _
.Member(m => m.Email, m => m
.Email()
.WithExtraCode("ERR_EMAIL")
.And()
.MaxLength(100)
)
.Member(m => m.Name, m => m
.Optional()
.And()
.LengthBetween(8, 100)
.And()
.Rule(name => name.All(char.IsLetterOrDigit))
.WithMessage("Must contain only letter or digits")
)
.And()
.Rule(m => m.Age >= 18 || m.Name != null)
.WithPath("Name")
.WithMessage("Required for underaged user")
.WithExtraCode("ERR_NAME");
The next step is to create a validator. As its name stands - it validates objects according to the specification. It's also thread-safe so you can seamlessly register it as a singleton in your DI container.
var validator = Validator.Factory.Create(specification);
Validate the object!
var model = new UserModel(email: "inv@lidv@lue", age: 14);
var result = validator.Validate(model);
The result object contains all information about the errors. Without retriggering the validation process, you can extract the desired form of an output.
result.AnyErrors; // bool flag:
// true
result.MessageMap["Email"] // collection of messages for "Email":
// [ "Must be a valid email address" ]
result.Codes; // collection of all the codes from the model:
// [ "ERR_EMAIL", "ERR_NAME" ]
result.ToString(); // compact printing of codes and messages:
// ERR_EMAIL, ERR_NAME
//
// Email: Must be a valid email address
// Name: Required for underaged user
Features
Advanced fluent API, inline
No more obligatory if-ology around input models or separate classes wrapping just validation logic. Write specifications inline with simple, human-readable fluent API. Native support for properties and fields, structs and classes, nullables, collections, nested members, and possible combinations.
Specification<string> nameSpecification = s => s
.LengthBetween(5, 50)
.SingleLine()
.Rule(name => name.All(char.IsLetterOrDigit));
Specification<string> emailSpecification = s => s
.Email()
.And()
.Rule(email => email.All(char.IsLower))
.WithMessage("Must contain only lower case characters");
Specification<UserModel> userSpecification = s => s
.Member(m => m.Name, nameSpecification)
.WithMessage("Must comply with name rules")
.And()
.Member(m => m.PrimaryEmail, emailSpecification)
.And()
.Member(m => m.AlternativeEmails, m => m
.Optional()
.And()
.MaxCollectionSize(3)
.WithMessage("Must not contain more than 3 addresses")
.And()
.AsCollection(emailSpecification)
)
.And()
.Rule(user => {
return user.PrimaryEmail is null || user.AlternativeEmails?.Contains(user.PrimaryEmail) == false;
})
.WithMessage("Alternative emails must not contain the primary email address");
- Blog post about constructing specifications in Validot
- Guide through Validot's fluent API
- If you prefer the approach of having a separate class for just validation logic, it's also fully supported
Validators
Compact, highly optimized, and thread-safe objects to handle the validation.
Specification<BookModel> bookSpecification = s => s
.Optional()
.Member(m => m.AuthorEmail, m => m.Optional().Email())
.Member(m => m.Title, m => m.NotEmpty().LengthBetween(1, 100))
.Member(m => m.Price, m => m.NonNegative());
var bookValidator = Validator.Factory.Create(bookSpecification);
services.AddSingleton<IValidator<BookModel>>(bookValidator);
var bookModel = new BookModel() { AuthorEmail = "inv@lid_em@il", Price = 10 };
bookValidator.IsValid(bookModel);
// false
bookValidator.Validate(bookModel).ToString();
// AuthorEmail: Must be a valid email address
// Title: Required
bookValidator.Validate(bookModel, failFast: true).ToString();
// AuthorEmail: Must be a valid email address
bookValidator.Template.ToString(); // Template contains all of the possible errors:
// AuthorEmail: Must be a valid email address
// Title: Required
// Title: Must not be empty
// Title: Must be between 1 and 100 characters in length
// Price: Must not be negative
Results
Whatever you want. Error flag, compact list of codes, or detailed maps of messages and codes. With sugar on top: friendly ToString() printing that contains everything, nicely formatted.
var validationResult = validator.Validate(signUpModel);
if (validationResult.AnyErrors)
{
// check if a specific code has been recorded for Email property:
if (validationResult.CodeMap["Email"].Contains("DOMAIN_BANNED"))
{
_actions.NotifyAboutDomainBanned(signUpModel.Email);
}
var errorsPrinting = validationResult.ToString();
// save all message
