Web API Controllers
|
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. |
Controller-based Web APIs give you conventions, filters, and the [ApiController] behaviors on top of routing
and model binding. Choose them over Minimal APIs for large APIs that
benefit from shared base classes, action filters, or an existing MVC codebase.
ControllerBase + [ApiController]
[ApiController]
[Route("api/[controller]")]
public sealed class OrdersController(IOrderService orders) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<ActionResult<OrderDto>> Get(int id)
{
var order = await orders.FindAsync(id);
return order is null ? NotFound() : Ok(order.ToDto());
}
[HttpPost]
public async Task<ActionResult<OrderDto>> Create(CreateOrderDto dto)
{
var created = await orders.CreateAsync(dto);
return CreatedAtAction(nameof(Get), new { id = created.Id }, created.ToDto());
}
}
[ApiController] enables automatic 400 responses for invalid ModelState, binding-source inference (complex
types from the body, simple types from the route/query), and ProblemDetails for error status codes. See
Create web APIs with ASP.NET Core.
Action return types
| Return type | Use |
|---|---|
|
Return either |
|
When only status/results matter, no typed body. |
|
Typed union; documents every response for OpenAPI (same as Minimal APIs). |
|
Plain object, serialized with 200. |
Status helpers: Ok, Created / CreatedAtAction, NoContent, NotFound, BadRequest, Conflict,
UnprocessableEntity, Problem, ValidationProblem. See
Controller action return types.
Content negotiation and System.Text.Json options
The framework picks an output formatter from the Accept header (JSON by default). Configure
System.Text.Json:
builder.Services.AddControllers(o => o.ReturnHttpNotAcceptable = true) // 406 instead of ignoring Accept
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
})
.AddXmlSerializerFormatters();
AddJsonOptions configures the MVC formatters. (ConfigureHttpJsonOptions is the equivalent for
Minimal APIs and does not affect controllers.)
Use [Produces("application/json")] / [Consumes("application/json")] to constrain a controller, and the
System.Text.Json source generator (JsonSerializerContext) for trimming/AOT and speed. Write a custom
TextOutputFormatter for formats like CSV. See
Format response data.
ProblemDetails error responses
Return RFC 9457 ProblemDetails / ValidationProblemDetails for machine-readable errors:
builder.Services.AddProblemDetails(o => o.CustomizeProblemDetails = ctx =>
ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier);
// in an action
return Problem(title: "Payment declined", statusCode: StatusCodes.Status402PaymentRequired);
Map exceptions to problem responses centrally with IExceptionHandler — see
Error Handling, Logging, and Observability
and Handle errors in ASP.NET Core web
APIs.
DTOs and API contracts
Expose request/response DTOs, not EF Core entities: it decouples the wire contract from the schema, prevents over-posting, and keeps navigation properties out of responses.
public sealed record OrderDto(int Id, string Customer, decimal Total, string Status);
public static class OrderMappings
{
public static OrderDto ToDto(this Order o) => new(o.Id, o.Customer.Name, o.Total, o.Status.ToString());
}
See Data Access with EF Core for the entities these DTOs are mapped from.
CORS
builder.Services.AddCors(o => o.AddPolicy("spa", p =>
p.WithOrigins("https://app.example.com").AllowAnyHeader().AllowAnyMethod()));
app.UseCors("spa");
See What is CORS?.
JsonPatch
Add the Microsoft.AspNetCore.JsonPatch.SystemTextJson package (.NET 10, System.Text.Json based), accept a
JsonPatchDocument<T> body and call patch.ApplyTo(model); the legacy path is the
Microsoft.AspNetCore.Mvc.NewtonsoftJson package with AddControllers().AddNewtonsoftJson(). See
JsonPatch in ASP.NET Core web API.
Minimal APIs vs. controllers
| Choose | When |
|---|---|
New, small-to-medium JSON APIs; least ceremony; fastest startup and best Native-AOT support. |
|
Controllers |
Large APIs that benefit from shared base-class logic, |
Both share the same routing, model binding, and hosting — the choice is about ergonomics for the app’s size
and team, not capability. .http files in the project let you send requests straight from the editor for
manual testing. See
Choose between controller-based APIs and
Minimal APIs.
Content negotiation
Accept: application/xml"] --> NEG{"Negotiate:
match Accept to a formatter"} NEG -- "xml formatter registered" --> XML["XmlSerializerOutputFormatter"] NEG -- "no match, ReturnHttpNotAcceptable" --> S406["406 Not Acceptable"] NEG -- "no match, default" --> JSON["SystemTextJsonOutputFormatter"] XML --> BODY["Serialized response body"] JSON --> BODY