The foundation: what the official documentation explains
ASP.NET Core distinguishes binding from validation: obtaining a value does not establish that the input is acceptable. MVC provides a model-validation system, while an application can also invoke a validation library explicitly. FluentValidation supports manual validation, including asynchronous execution. Registering validators alone does not demonstrate that a specific handler or endpoint actually runs them.
Implementation context: The examples use a .NET 10 MVC application organized into feature folders (Vertical Slice Architecture). Basic C# classes and async/await are assumed. Reference excerpts show selected parts of that application; separately labeled teaching adaptations explain alternatives. They are not complete standalone projects.
Start with rules the user can understand
For Country, the server requires a name and limits its length to 100 characters. Code is optional but capped at 50 characters. Description is optional and capped at 500. The name rule explains why a nullable request property can still be mandatory at the business boundary: the DTO represents input, including invalid input, and the validator decides what is allowed.
This is a focused example from a .NET 10 MVC application using FluentValidation 12.1.1. FluentValidation is a third-party package, not a built-in VSA or MVC requirement. The official ASP.NET Core documentation explains the general validation concept; the project code demonstrates one chosen implementation.
Reference excerpt: Areas/Admin/Country/Cqrs/CreateCountryValidator.cs
using FluentValidation;
namespace Indotalent.Areas.Admin.Country.Cqrs;
public class CreateCountryValidator : AbstractValidator<CreateCountryRequest>
{
public CreateCountryValidator()
{
RuleFor(x => x.Code)
.MaximumLength(50).WithMessage("Country Code must not exceed 50 characters");
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Country Name is required")
.MaximumLength(100).WithMessage("Country Name must not exceed 100 characters");
RuleFor(x => x.Description)
.MaximumLength(500).WithMessage("Description must not exceed 500 characters");
}
}
Find the actual validation call
The create handler constructs the validator and awaits ValidateAsync before checking duplicate codes or creating an entity. This is explicit manual validation. Although startup also scans the assembly for validators, this handler uses new directly. Do not credit assembly scanning with behavior caused by the call below.
Reference excerpt: Areas/Admin/Country/Cqrs/CreateCountryHandler.cs
var validator = new CreateCountryValidator();
var validationResult = await validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid)
{
return ApiResponse<CreateCountryResponse>.Fail("Validation failed",
validationResult.ToDictionary());
}
When validation fails, the handler returns immediately. The endpoint maps that failed response to BadRequest. The database-write section is never reached for this failure path. Keeping that order visible makes the code easier to review than relying on a reader to discover a hidden validation convention.
Separate field rules from database-dependent rules
After input validation, the handler checks whether a nonblank country code already exists. This is a business query. It cannot be replaced by checking the text length, and it can fail even when every field is syntactically valid. The application reports a general message for this duplicate rather than pretending it is a JSON parsing problem.
A pre-insert existence query improves feedback but does not by itself make concurrent inserts atomic. Two requests can both pass the query before either saves. Database constraints and transaction behavior must be considered separately for invariants that require concurrency protection. This observation describes the boundary of the example; it is not a reason to skip user-friendly validation.
Make field errors consumable by the browser
The response wrapper holds a dictionary of property names and arrays of messages. The Create page reads result.errors and places the first message into its reactive errors object. Its inputs look up lower-case keys such as errors.name. Verify the serialized dictionary key shape before assuming those messages appear next to the right fields.
A simple adaptation is to normalize the first character when mapping server property keys to the existing camelCase form fields. That mapping is a client convention, not a validator rule. The following code illustrates the mapping boundary; the reference page currently copies keys directly.
Teaching adaptation inside the existing Vue submit error branch
for (const [key, messages] of Object.entries(result.errors ?? {})) {
const field = key.charAt(0).toLowerCase() + key.slice(1);
errors[field] = messages[0];
}
Keep client validation as immediate feedback
The browser checks the same visible limits before submitting. This avoids a needless round trip for an empty name, but clients can bypass that code. An API client, an old browser bundle, or a future integration can still send invalid data. The handler remains responsible for enforcing its input rules.
For a practical check in a local copy, submit a blank name directly to the API, then names with exactly 100 and 101 characters. Repeat with a duplicate code. Confirm both the failed outcome and that no unwanted row was saved. A UI that shows a red border is useful feedback, but it is not proof of server enforcement.
Do not confuse input limits with storage guarantees
The application also configures database column properties separately. A validator accepting a value does not prove the current database schema can store it. If an accepted description fails on save, compare the validator, entity configuration, and actual column. Keep validation messages aligned with the deployed schema rather than adding a catch-all success response around persistence errors.
Key Takeaways
- Identify the explicit validation invocation before assuming automation.
- Keep browser feedback and server enforcement aligned.
- Distinguish field errors, duplicate checks, and persistence failures.
FAQ
Does AddValidatorsFromAssembly automatically validate this request?
The reference handler explicitly calls ValidateAsync. Registration alone is not the mechanism that runs that call.
Can I trust browser validation?
Use it for feedback, but the server must validate independently.
Is checking for duplicates enough under concurrency?
No. A preliminary query alone cannot prevent two simultaneous writes from passing the same check.