Model Binding and Validation
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
Model binding turns raw request data — form fields, route values, query string, headers, JSON body — into the .NET objects your action methods and Razor Pages declare. Validation then checks those objects before you use them.
Binding sources and order
For a non-[ApiController] MVC action, model binding looks for each value in this order:
-
Form values (
POSTbodies ofapplication/x-www-form-urlencoded/multipart/form-data) -
Route values
-
Query string
The JSON/XML request body is bound only for parameters marked [FromBody] (implicit under [ApiController]
for complex types). Binding handles simple types, complex objects, collections, dictionaries, DateOnly /
TimeOnly, and uploaded files.
// GET /products?ids=1&ids=2&sort=name -> int[] ids, string sort
public IActionResult Search(int[] ids, string sort) => ...;
// multipart form upload
public async Task<IActionResult> Upload(IFormFile file, [FromForm] string caption)
{
await using var stream = file.OpenReadStream();
// ...
return Ok();
}
Binding attributes
| Attribute | Effect |
|---|---|
|
Force a specific source. |
|
Add a model-state error if no value was supplied. |
|
Never bind this property (e.g. an audit field). |
|
Restrict which properties bind; |
|
Use a custom binder for this parameter. |
Input formatters handle the body: System.Text.Json by default; add the XML formatter with
AddControllers().AddXmlSerializerFormatters().
Custom model binding
Implement IModelBinder (and an IModelBinderProvider to select it) when a value needs non-standard parsing:
public sealed class CommaSeparatedArrayBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
var parts = (value ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
bindingContext.Result = ModelBindingResult.Success(parts);
return Task.CompletedTask;
}
}
// usage
public IActionResult Tags([ModelBinder(typeof(CommaSeparatedArrayBinder))] string[] tags) => Ok(tags);
For simple route/query cases, a static bool TryParse(string, IFormatProvider?, out T) on the type is enough — no binder needed. See
Custom model binding.
Validation
Annotate the model with DataAnnotations; check ModelState.IsValid (automatic 400 under [ApiController]):
public sealed class ProductInput : IValidatableObject
{
[Required, StringLength(100)] public string Name { get; set; } = "";
[Range(0.01, 100_000)] public decimal Price { get; set; }
[Required, EmailAddress] public string ContactEmail { get; set; } = "";
[RegularExpression("^[A-Z]{3}$")] public string CurrencyCode { get; set; } = "USD";
// cross-field / model-level rule
public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
{
if (CurrencyCode == "USD" && Price > 10_000)
yield return new ValidationResult("USD orders over 10,000 need approval", [nameof(Price)]);
}
}
A custom attribute encapsulates a reusable rule:
public sealed class NotInPastAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
=> value is DateOnly d && d < DateOnly.FromDateTime(DateTime.UtcNow)
? new ValidationResult(ErrorMessage ?? "Date is in the past")
: ValidationResult.Success;
}
Client-side validation
In Razor views, asp-validation-for / asp-validation-summary plus the unobtrusive-validation scripts
(jquery.validate + jquery.validate.unobtrusive) enforce the same DataAnnotations in the browser before
submit. [Remote] calls back to a controller action for server-only checks (e.g. "is this username taken?").
<div asp-validation-summary="ModelOnly"></div>
<input asp-for="ContactEmail" />
<span asp-validation-for="ContactEmail"></span>
@section Scripts { <partial name="_ValidationScriptsPartial" /> }
With nullable reference types enabled, a non-nullable reference property is treated as implicitly [Required].
See
client-side
validation.