.NETMedium
ASP.NET Core model binding — how it works and how to extend it
Model binding maps HTTP request data (route, query, body, form, header) to action-method parameters using value providers + model binders.
Default source order
For a parameter Foo(MyModel m) ASP.NET tries in this order:
[FromForm]— form fields (multipart/url-encoded)- Route data
- Query string
- (For
[ApiController]) request body for complex types
Override explicitly:
public IActionResult Update(
[FromRoute] Guid id,
[FromQuery] bool dryRun,
[FromHeader(Name = "X-Correlation-Id")] string correlationId,
[FromBody] UpdateOrderRequest body) { ... }
Custom binder example — parse a comma-separated list
public class CsvBinder<T> : IModelBinder where T : IParsable<T>
{
public Task BindModelAsync(ModelBindingContext ctx)
{
var raw = ctx.ValueProvider.GetValue(ctx.ModelName).FirstValue;
if (string.IsNullOrWhiteSpace(raw))
{ ctx.Result = ModelBindingResult.Success(Array.Empty<T>()); return Task.CompletedTask; }
var parsed = raw.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(s => T.Parse(s.Trim(), null))
.ToArray();
ctx.Result = ModelBindingResult.Success(parsed);
return Task.CompletedTask;
}
}
public class CsvBinderProvider : IModelBinderProvider {
public IModelBinder? GetBinder(ModelBinderProviderContext ctx)
=> ctx.Metadata.ModelType.IsArray
? new CsvBinder<int>() : null;
}
Register in Program.cs:
builder.Services.AddControllers(o => o.ModelBinderProviders.Insert(0, new CsvBinderProvider()));
Common production gotchas
[ApiController]enables automatic 400 on invalid model state — disable withSuppressModelStateInvalidFilterfor custom error envelopes.- Binding huge JSON bodies allocates eagerly — use
IAsyncEnumerable<T>+System.Text.Jsonstreaming for large payloads. - Decimal binding respects culture; in APIs always force
InvariantCultureviaJsonSerializerOptionsto avoid1.234vs1,234ambiguity across regions.