ASP.NET Validation - Quick Reference
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:aspnet-corefor validation documentation.
Data Annotations
public class CreateUserRequest
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = default!;
[Required]
[EmailAddress]
[StringLength(255)]
public string Email { get; set; } = default!;
[Required]
[Range(0, 150)]
public int Age { get; set; }
[Url]
public string? Website { get; set; }
}
FluentValidation Setup
// Install: dotnet add package FluentValidation.AspNetCore
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
FluentValidation Patterns
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
private readonly IUserRepository _repository;
public CreateUserValidator(IUserRepository repository)
{
_repository = repository;
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.Length(2, 100);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.MaximumLength(255)
.MustAsync(BeUniqueEmail).WithMessage("Email already exists");
RuleFor(x => x.Age)
.InclusiveBetween(0, 150);
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(12)
.Matches(@"[A-Z]").WithMessage("Must contain uppercase letter")
.Matches(@"[a-z]").WithMessage("Must contain lowercase letter")
.Matches(@"\d").WithMessage("Must contain digit")
.Matches(@"[@$!%*?&]").WithMessage("Must contain special character");
}
private async Task<bool> BeUniqueEmail(string email, CancellationToken ct)
{
return !await _repository.ExistsAsync(u => u.Email == email);
}
}
Validation Filter (Minimal API)
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (validator is not null && argument is not null)
{
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
}
return await next(context);
}
}
Collection and Nested Validation
public class OrderValidator : AbstractValidator<CreateOrderRequest>
{
public OrderValidator()
{
RuleFor(x => x.Items)
.NotEmpty().WithMessage("Order must have at least one item");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId).GreaterThan(0);
item.RuleFor(x => x.Quantity).InclusiveBetween(1, 1000);
});
}
}
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|--------------|--------------|------------------|
| Validation in controllers | Repetitive, inconsistent | Use FluentValidation + filter |
| Only client-side validation | Easily bypassed | Always validate server-side |
| No async validation | Can't check DB uniqueness | Use MustAsync |
| Generic error messages | Bad UX | Provide specific .WithMessage() |
Quick Troubleshooting
| Issue | Likely Cause | Solution |
|-------|--------------|----------|
| Validator not running | Not registered | Call AddValidatorsFromAssembly |
| Async validator timeout | DB query slow | Add appropriate indexes |
| ModelState not checked | Missing [ApiController] | Add attribute or check manually |
| Nested validation skipped | Missing SetValidator | Use ChildRules or SetValidator |
Scan to join WeChat group