Validates HTTP request inputs. .NET 10 AddValidation, FluentValidation, ProblemDetails.
Comprehensive input validation patterns for .NET APIs. Covers the .NET 10 built-in validation system, FluentValidation for complex business rules, Data Annotations for simple models, endpoint filters for Minimal API integration, ProblemDetails error responses, and security-focused validation techniques.
Cross-references: [skill:dotnet-security-owasp] for OWASP injection prevention, [skill:dotnet-architecture-patterns] for architectural validation strategy, [skill:dotnet-minimal-apis] for Minimal API pipeline integration, [skill:dotnet-csharp-configuration] for Options pattern validation.
Choose the validation framework based on project requirements:
AddValidation) -- default for new .NET 10+ projects. Source-generator-based,
AOT-compatible, auto-discovers types from Minimal API handlers. Best for: greenfield projects targeting .NET 10+.[Required], [Range]). Widely understood,
works with MVC model binding and IValidatableObject for cross-property checks. Best for: simple DTOs, shared
models.General guidance: prefer .NET 10 built-in validation for new projects. Use FluentValidation when rules outgrow annotations. Do not mix multiple frameworks in the same request DTO -- pick one per model type and stay consistent.
.NET 10 introduces Microsoft.Extensions.Validation with source-generator-based validation that integrates directly
into the Minimal API pipeline. It auto-discovers validatable types from endpoint handler parameters and runs validation
via an endpoint filter.
// <PackageReference Include="Microsoft.Extensions.Validation" Version="10.*" />
builder.Services.AddValidation();
var app = builder.Build();
// Validation runs automatically via endpoint filter for Minimal API handlers
```text
`AddValidation()` scans for types annotated with `[ValidatableType]` and generates validation logic at compile time
using source generators, ensuring Native AOT compatibility.
### Defining Validatable Types
```csharp
[ValidatableType]
public partial class CreateProductRequest
{
[Required]
[StringLength(200, MinimumLength = 1)]
public required string Name { get; set; }
[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
[Required]
[RegularExpression(@"^[A-Z]{2,4}-\d{4,8}$", ErrorMessage = "SKU format: AA-0000")]
public required string Sku { get; set; }
}
```text
The `partial` keyword is required because the source generator emits validation logic into the same type. The
`[ValidatableType]` attribute triggers code generation at compile time -- no reflection at runtime.
### How It Works
1. Source generator discovers `[ValidatableType]` classes and emits `IValidatableObject`-like validation logic.
2. `AddValidation()` registers an endpoint filter that inspects Minimal API handler parameters.
3. When a request arrives, the filter validates parameters before the handler executes.
4. On failure, returns a `ValidationProblem` response automatically.
**Gotcha:** `AddValidation()` integrates with Minimal APIs via endpoint filters. MVC controllers use their own model
validation pipeline and do not participate in this filter-based system. For controllers, Data Annotations and
`ModelState.IsValid` remain the standard approach.
---
## FluentValidation
FluentValidation provides a fluent API for building strongly-typed validation rules. It excels at complex business
validation with cross-property rules, conditional logic, and database-dependent checks.
### Validator Definition
```csharp
// <PackageReference Include="FluentValidation" Version="11.*" />
// <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.*" />
public sealed class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty()
.MaximumLength(50);
RuleFor(x => x.OrderDate)
.LessThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow))
.WithMessage("Order date cannot be in the future");
RuleFor(x => x.Lines)
.NotEmpty()
.WithMessage("Order must have at least one line item");
RuleForEach(x => x.Lines)
.ChildRules(line =>
{
line.RuleFor(l => l.ProductId).NotEmpty();
line.RuleFor(l => l.Quantity).GreaterThan(0);
line.RuleFor(l => l.UnitPrice).GreaterThan(0);
});
// Conditional rule
When(x => x.ShippingMethod == ShippingMethod.Express, () =>
{
RuleFor(x => x.ShippingAddress)
.NotNull()
.WithMessage("Express shipping requires an address");
});
}
}
```text
### DI Registration with Assembly Scanning
```csharp
// Registers all AbstractValidator<T> implementations from the assembly
builder.Services.AddValidatorsFromAssemblyContaining<Program>(ServiceLifetime.Scoped);
```csharp
### Manual Validation Pattern (Recommended)
FluentValidation's ASP.NET pipeline auto-validation is deprecated. Use manual validation in endpoint handlers or
endpoint filters instead:
```csharp
app.MapPost("/api/orders", async (
CreateOrderRequest request,
IValidator<CreateOrderRequest> validator,
AppDbContext db) =>
{
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
{
return TypedResults.ValidationProblem(result.ToDictionary());
}
var order = new Order { CustomerId = request.CustomerId };
db.Orders.Add(order);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/orders/{order.Id}", order);
});
```text
### FluentValidation Endpoint Filter
For reusable validation across multiple endpoints, create a generic endpoint filter (see also
[skill:dotnet-minimal-apis] for filter pipeline details):
```csharp
public sealed class FluentValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument is null)
return TypedResults.BadRequest("Request body is required");
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
return TypedResults.ValidationProblem(result.ToDictionary());
return await next(context);
}
}
// Apply to endpoints
products.MapPost("/", CreateProduct)
.AddEndpointFilter<FluentValidationFilter<CreateProductDto>>();
```text
**Gotcha:** Do not use the deprecated `FluentValidation.AspNetCore` auto-validation pipeline. It was removed in
FluentValidation 11. Use manual validation or endpoint filters as shown above.
---
## Data Annotations
Data Annotations provide declarative validation through attributes. They work with MVC model binding, Minimal API
binding, and the .NET 10 `AddValidation()` source generator.
### Standard Attributes
```csharp
public sealed class UpdateProductDto
{
[Required(ErrorMessage = "Product name is required")]
[StringLength(200, MinimumLength = 1)]
public required string Name { get; set; }
[Range(0.01, 1_000_000, ErrorMessage = "Price must be between {1} and {2}")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]{2,4}-\d{4,8}$")]
public string? Sku { get; set; }
[EmailAddress]
public string? ContactEmail { get; set; }
[Url]
public string? WebsiteUrl { get; set; }
[Phone]
public string? SupportPhone { get; set; }
}
```text
### Custom ValidationAttribute
```csharp
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext context)
{
if (value is DateOnly date && date <= DateOnly.FromDateTime(DateTime.UtcNow))
{
return new ValidationResult(
ErrorMessage ?? "Date must be in the future",
new[] { context.MemberName! });
}
return ValidationResult.Success;
}
}
// Usage
public sealed class CreateEventDto
{
[Required]
[StringLength(200)]
public required string Title { get; set; }
[FutureDate(ErrorMessage = "Event date must be in the future")]
public DateOnly EventDate { get; set; }
}
```text
### IValidatableObject for Cross-Property Validation
```csharp
public sealed class DateRangeDto : IValidatableObject
{
[Required]
public DateOnly StartDate { get; set; }
[Required]
public DateOnly EndDate { get; set; }
[Range(1, 365)]
public int MaxDays { get; set; } = 30;
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (EndDate < StartDate)
{
yield return new ValidationResult(
"End date must be after start date",
new[] { nameof(EndDate) });
}
if ((EndDate.ToDateTime(TimeOnly.MinValue) - StartDate.ToDateTime(TimeOnly.MinValue)).Days > MaxDays)
{
yield return new ValidationResult(
$"Date range cannot exceed {MaxDays} days",
new[] { nameof(StartDate), nameof(EndDate) });
}
}
}
```text
**Gotcha:** Options pattern classes must use `{ get; set; }` not `{ get; init; }` because the configuration binder needs
to mutate properties after construction. Validation attributes on `init`-only properties work for request DTOs but fail
for options classes bound via `IConfiguration`. See [skill:dotnet-csharp-configuration] for Options pattern validation.
---
## Endpoint Filters for Validation
Endpoint filters integrate validation into the Minimal API request pipeline as a cross-cutting concern. Filters execute
before the handler, enabling centralized validation logic.
### Generic Data Annotations Filter
```csharp
public sealed class DataAnnotationsValidationFilter<T> : IEndpointFilter
where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument is null)
return TypedResults.BadRequest("Request body is required");
var validationResults = new List<ValidationResult>();
var validationContext = new ValidationContext(argument);
if (!Validator.TryValidateObject(argument, validationContext, validationResults, validateAllProperties: true))
{
var errors = validationResults
.Where(r => r.MemberNames.Any())
.GroupBy(r => r.MemberNames.First())
.ToDictionary(
g => g.Key,
g => g.Select(r => r.ErrorMessage ?? "Validation failed").ToArray());
return TypedResults.ValidationProblem(errors);
}
return await next(context);
}
}
// Apply to endpoints or route groups
products.MapPost("/", CreateProduct)
.AddEndpointFilter<DataAnnotationsValidationFilter<CreateProductDto>>();
products.MapPut("/{id:int}", UpdateProduct)
.AddEndpointFilter<DataAnnotationsValidationFilter<UpdateProductDto>>();
```text
## Detailed Examples
See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.