FlowValidate.AspNetCore
1.3.0
See the version list below for details.
dotnet add package FlowValidate.AspNetCore --version 1.3.0
NuGet\Install-Package FlowValidate.AspNetCore -Version 1.3.0
<PackageReference Include="FlowValidate.AspNetCore" Version="1.3.0" />
<PackageVersion Include="FlowValidate.AspNetCore" Version="1.3.0" />
<PackageReference Include="FlowValidate.AspNetCore" />
paket add FlowValidate.AspNetCore --version 1.3.0
#r "nuget: FlowValidate.AspNetCore, 1.3.0"
#:package FlowValidate.AspNetCore@1.3.0
#addin nuget:?package=FlowValidate.AspNetCore&version=1.3.0
#tool nuget:?package=FlowValidate.AspNetCore&version=1.3.0
NuGet Package Information
| Package | Downloads | License |
|---|---|---|
Repository
You can find the source code and contribute on GitHub
FlowValidate
FlowValidate is a lightweight, fluent-style validation library for .NET.
It provides an intuitive API for validating models, making it easy to add and enforce rules while reducing boilerplate code.
Targets net6.0, net7.0, net8.0, net9.0 and net10.0.
Features
- Property Validation: Validate standard properties, nested objects, and collections.
- Nested & Collection Support: Automatically validates complex types and lists.
- Custom Rules: Use
Should,Must,IsNotEmpty,IsEqualor define your own logic. - Typed Ranges:
IsInRange,IsGreaterThanandIsLessThanwork ondecimal,double,long,DateTimeand any otherIComparable<T>type, including nullable ones. - Multi-error per Rule: Single property rules can produce multiple error messages.
- Reusable & Property-specific Validators: Create modular validators like
UserNameValidatorand apply them to properties. - Async / Task-based Validation: Rules can run asynchronously (
MustAsync/ShouldAsync) with a synchronous validation fallback bridge. - DI Support: Easy integration with dependency injection.
- Clear Error Messages: Provides detailed validation feedback.
- Detailed Error Messages: Provides rich validation feedback with property name, attempted value, and optional error code.
- Lightweight & Fast: Optimized for high performance.
- Middleware Ready: Can validate models automatically on each request with the separate
FlowValidate.AspNetCorepackage.
Installation
You can install FlowValidate via NuGet Package Manager
dotnet add package FlowValidate
To validate ASP.NET Core requests automatically, also install the middleware package:
dotnet add package FlowValidate.AspNetCore
FlowValidate contains the validation API only. Console, worker and other non-web apps need just this package.
Injection
using FlowValidate.AspNetCore;
using FlowValidate.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.FlowValidationService(AssemblyReference.Assembly);
var app = builder.Build();
app.UseFlowValidation();
app.Run();
Calling FlowValidationService more than once with the same assembly is safe. The repeated call does not add duplicate validator or assembly registrations. Validators stay Scoped. Calls with different assemblies register all of them, and the middleware keeps scanning the most recently registered assembly.
When a controller action's model fails validation, UseFlowValidation() responds with 400 Bad Request and a JSON body:
{"Errors":[{"PropertyName":"Name","ErrorMessage":"Name is required.","AttemptedValue":null,"ErrorCode":"NAME_REQUIRED","Severity":2}]}
Which action parameter is validated
UseFlowValidation() reads the request body for a single parameter of the matched action: the one marked [FromBody], or — when no parameter is marked — the single complex-typed parameter that has no binding source attribute. Parameters bound from somewhere else ([FromQuery], [FromRoute], [FromHeader], [FromForm], [FromServices]) and simple types such as int or string are never deserialized from the body, so a mixed signature like Create([FromBody] Order order, [FromQuery] OrderFilter filter) validates order only. If the action has no parameter that binds from the body, the request passes through untouched and the body is not read at all. The body is buffered and rewound, so the model binder and later middleware still read it normally.
Middleware limits
- Requires routing to have run first. The middleware reads
HttpContext.GetRouteData()to find the matched controller and action, soapp.UseFlowValidation()must be registered afterapp.UseRouting()(or after endpoint routing has otherwise populated route values). If route data has nocontroller/action— request didn't match any route, or the middleware runs before routing — the request passes through untouched and no validation happens. - No registered validator, no error. If no
IBaseValidator<T>is registered in DI for the resolved body-parameter type, the middleware also passes the request through untouched; it does not throw or report a missing validator. - Body deserialization uses
Newtonsoft.Json, independent of whatever JSON stack the host application uses for model binding (System.Text.Jsonby default in ASP.NET Core). A type that binds correctly through the host's own JSON settings can still be deserialized differently by the middleware if the two serializers disagree (e.g. custom converters, naming policies).
Migrating from FlowValidationApp()
app.FlowValidationApp() and ModelValidationMiddleware in the core package are obsolete. They keep working in this version and will be removed from the core package in the next major version. To migrate, install FlowValidate.AspNetCore and replace app.FlowValidationApp() with app.UseFlowValidation(). The error body is identical. One difference: the obsolete FlowValidationApp() answers invalid requests with 200 OK, while UseFlowValidation() answers them with 400 Bad Request. If a client checks for 200 together with the Errors body, update the client when you migrate.
For example, we create a uservalidator
Reusable Registry Rule
public class UserNameValidator : BaseValidator<string>
{
public UserNameValidator()
{
RuleFor(name => name).IsNotEmpty().WithMessage("Name cannot be empty.")
.Length(3, 100).WithMessage("Name must be at least 3 characters.");
}
}
Nested Validator
public class UserDetailsValidator : BaseValidator<UserDetails>
{
public UserDetailsValidator()
{
RuleFor(x => x.Email).IsEmail().WithMessage("Email is invalid.");
RuleFor(x => x.Phone).MatchesRegex(@"^\d{10}$").WithMessage("Phone must be 10 digits.");
}
}
Collection Validator
public class UserBasketValidator : BaseValidator<UserBasket>
{
public UserBasketValidator()
{
RuleFor(x => x.Name).IsNotEmpty();
RuleFor(x => x.Count).IsGreaterThan(0);
}
}
Main User Validator
public class UserValidator : BaseValidator<User>
{
public UserValidator()
{
// Registry rule
ValidateRegistryRules(u => u.Name, new UserNameValidator());
// Nested validator
ValidateNested(u => u.Details, new UserDetailsValidator());
// Collection validator
ValidateCollection(u => u.Baskets, new UserBasketValidator(), item => item);
// Custom validation with Should
RuleFor(u => u.Nickname)
.Should((nickname, addError) =>
{
if (!string.IsNullOrEmpty(nickname))
{
if (nickname.Length < 3)
addError("Nickname must be at least 3 characters long.");
if (nickname.Contains(" "))
addError("Nickname cannot contain spaces.");
}
});
}
}
Builder Types Returned by ValidateNested, ValidateCollection and ValidateRegistryRules
ValidateNested, ValidateCollection and ValidateRegistryRules return ValidationNestedBuilder<T, TProperty>, ValidationCollectionBuilder<T, TCollection, TElement> and ValidationRegistryRules<T, TProperty>. Today each of the three exposes only one public member, Task<ValidationResult> ValidateAsync(T instance). RuleFor already wires the returned builder into the parent validator's rule pipeline, so Validate(user) / ValidateAsync(user) on UserValidator runs it automatically — you do not need to call ValidateAsync on the child builder yourself. It is there mainly so a test can exercise one composition step in isolation, for example await new UserDetailsValidator().ValidateAsync(details) directly, or await new ValidationNestedBuilder<User, UserDetails>(u => u.Details, new UserDetailsValidator()).ValidateAsync(user).
Conditional Rules with RequiredIf
RequiredIf(Func<TProperty, bool> condition) gates the rules chained after it behind a check on the property's own value. When condition(value) is false, the rule fails immediately with "Property is required." (errorCode: "Required") and every rule chained after RequiredIf on that property is skipped (ValidationResult.SetSkipRemainingRules(true)) — other properties' RuleFor chains are unaffected. When condition(value) is true, RequiredIf itself only fails if the value is null or, for string, blank; on success the remaining chained rules run as usual.
public class PromoValidator : BaseValidator<Promo>
{
public PromoValidator()
{
RuleFor(x => x.PromoCode)
.RequiredIf(code => code is not null && code.StartsWith("PROMO"))
.Length(8, 12)
.Contains("-");
}
}
PromoCode = "ABC"→ condition isfalse→ fails with"Property is required.";Length/Containsdo not run.PromoCode = "PROMO12"→ condition istrue→Length/Containsrun and report their own failures.PromoCode = "PROMO-123"→ condition istrueand the remaining rules pass → valid.
Severity
ValidationFailure.Severity is a FlowValidate.Enums.Severity value (Info, Warning, Error). Every failure produced by the built-in rules, Should/ShouldAsync, and RequiredIf defaults to Severity.Error — there is no fluent option on RuleFor/WithMessage to change it. To report a lower severity, construct the failure yourself, either with ValidationResult.Failure(message, propertyName, attemptedValue, errorCode, severity) or new ValidationFailure(...), and merge it into the validator's result:
var result = validator.Validate(user);
result.Merge(ValidationResult.Failure(
"Backup email is missing.",
propertyName: "BackupEmail",
severity: Severity.Warning));
Severity is also part of the middleware's JSON error body (see the Errors[].Severity field above).
Asynchronous Validation (Async Support)
FlowValidate fully supports asynchronous validation rules for operations that require external or asynchronous calls (e.g., database queries or external API requests). You can use MustAsync and ShouldAsync inside your validators.
Asynchronous Rules Example
public class UserValidator : BaseValidator<User>
{
private readonly IUserRepository _userRepository;
public UserValidator(IUserRepository userRepository)
{
_userRepository = userRepository;
// Using MustAsync for custom async boolean conditions
RuleFor(u => u.Email)
.MustAsync(async email => !await _userRepository.ExistsAsync(email))
.WithMessage("This email address is already in use.");
// Using ShouldAsync with a multi-error delegate callback
RuleFor(u => u.Username)
.ShouldAsync(async (username, addError) =>
{
var isBlacklisted = await _userRepository.IsBlacklistedAsync(username);
if (isBlacklisted)
{
addError("Username is blacklisted.");
}
});
// Using ShouldAsync with an action exception boundary
RuleFor(u => u.Bio)
.ShouldAsync(async bio =>
{
await _userRepository.ValidateBioFormatAsync(bio); // Throws if invalid
}, "Bio format is invalid.");
}
}
Executing Asynchronously vs Synchronously
var validator = new UserValidator(userRepository);
// 1. Asynchronous execution (Recommended when using async rules)
var resultAsync = await validator.ValidateAsync(user);
// 2. Synchronous execution bridge (Executes async rules synchronously under the hood)
var resultSync = validator.Validate(user);
Using the Validator
var user = new User
{
Name = "Jo",
Age = 25,
Details = new UserDetails { Email = "invalid-email", Phone = "12345" },
Baskets = new List<UserBasket>
{
new UserBasket { Name = "", Count = 0 },
new UserBasket { Name = "Apple", Count = 3 }
}
};
var validator = new UserValidator();
var result = validator.Validate(user);
Property Names in Failures
Every ValidationFailure.PropertyName is non-null. A plain member access reports the member name; any other expression reports its expression text.
| Rule | PropertyName |
|---|---|
RuleFor(x => x.Name) |
Name |
RuleFor(x => x.Details.Email) |
Email |
RuleFor(x => x.Tags[0]) (array) |
x.Tags[0] |
RuleFor(x => x.Baskets[0]) (list) |
x.Baskets.get_Item(0) |
RuleFor(x => x.Name.Trim()) |
x.Name.Trim() |
RuleFor(x => x) |
x |
Ranges and Comparisons for Any Ordered Type
IsInRange, IsGreaterThan and IsLessThan work on any property type that implements IComparable<T>: decimal, double, long, DateTime, DateOnly, string and their nullable forms. Pass bounds of the property's own type; a bound of another type does not compile.
public class ProductValidator : BaseValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Price).IsInRange(0.01m, 999.99m);
RuleFor(x => x.Rating).IsGreaterThan(0.0).IsLessThan(5.5);
RuleFor(x => x.Views).IsLessThan(10_000_000_000L);
RuleFor(x => x.ReleasedAt).IsGreaterThan(new DateTime(2020, 1, 1));
RuleFor(x => x.Discount).IsInRange(0m, 0.5m);
}
}
IsInRangeincludes both bounds;IsGreaterThanandIsLessThanare strict.- A
nullvalue fails these rules. For an optional property that may benull, useMust, for exampleRuleFor(x => x.Discount).Must(d => d is null or (>= 0m and <= 0.5m)). - Integer bounds such as
IsInRange(1, 10)still bind to the originalintoverloads, which behave exactly as before and only acceptint-convertible values. On adecimal,doubleorlongproperty, write the bounds with the matching literal suffix (1m,1.0,1L).
For more examples and unit tests, check the FlowValidate.Test project in the repository.
Want to contribute? See CONTRIBUTING.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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 was computed. 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. |
-
net6.0
- FlowValidate (>= 1.3.0)
- Newtonsoft.Json (>= 13.0.4)
-
net7.0
- FlowValidate (>= 1.3.0)
- Newtonsoft.Json (>= 13.0.4)
-
net8.0
- FlowValidate (>= 1.3.0)
- Newtonsoft.Json (>= 13.0.4)
-
net9.0
- FlowValidate (>= 1.3.0)
- Newtonsoft.Json (>= 13.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Cache controller/action to parameter-type lookups instead of reflecting over the whole assembly on every request, and validate only the body-bound action parameter instead of re-reading the same request body for every parameter.