Controllers and MVC
Organize APIs with controller classes — attribute routing, model binding, validation and filters.
12 min read
You will learn
- Write an API controller with attribute routing
- Use automatic model validation with [ApiController]
- Apply filters for cross-cutting concerns
- Choose between controllers and minimal APIs
The class-based style
Before minimal APIs, ASP.NET Core APIs were written with controllers: classes grouping related endpoints ("actions") as methods. You'll find them in most existing .NET codebases, and they remain fully supported.
builder.Services.AddControllers();
...
app.MapControllers();[ApiController]
[Route("api/[controller]")] // → api/products
public class ProductsController(ShopDb db) : ControllerBase
{
[HttpGet]
public async Task<IEnumerable<ProductDto>> GetAll() =>
await db.Products.Select(p => new ProductDto(p.Id, p.Name, p.Price)).ToListAsync();
[HttpGet("{id:int}")] // → api/products/42
public async Task<ActionResult<ProductDto>> Get(int id)
{
var p = await db.Products.FindAsync(id);
if (p is null) return NotFound();
return new ProductDto(p.Id, p.Name, p.Price);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> Create(CreateProductRequest request)
{
var product = new Product { Name = request.Name, Price = request.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = product.Id }, new ProductDto(product.Id, product.Name, product.Price));
}
[HttpDelete("{id:int}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(int id) { ... return NoContent(); }
}ControllerBaseprovides helpers:Ok(),NotFound(),BadRequest(),CreatedAtAction(),NoContent().- Attribute routing (
[Route],[HttpGet("{id}")]) defines URLs right on the actions. - Dependencies arrive through the constructor (DI).
ActionResult<T>lets an action return either a value or a status result.
Model binding and validation
Parameters bind from route, query, body and headers just like minimal APIs. Add DataAnnotations to request models:
public class CreateProductRequest
{
[Required, StringLength(200)]
public string Name { get; set; } = "";
[Range(0.01, 100_000)]
public decimal Price { get; set; }
[EmailAddress]
public string? SupplierEmail { get; set; }
}With [ApiController], invalid input never reaches your action: the framework responds 400 Bad Request with Problem Details listing every invalid field. For more complex rules, FluentValidation is a popular library.
Filters
Filters run around action execution — MVC's equivalent of middleware, but aware of controllers and actions:
| Filter | Runs |
|---|---|
| Authorization filters | First — can reject the request |
| Resource filters | Before model binding (e.g. caching) |
| Action filters | Before and after the action method |
| Exception filters | When an action throws |
| Result filters | Before and after the result is written |
public class TimingFilter(ILogger<TimingFilter> logger) : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var sw = Stopwatch.StartNew();
await next(); // run the action
logger.LogInformation("{Action} took {Ms} ms", context.ActionDescriptor.DisplayName, sw.ElapsedMilliseconds);
}
}
builder.Services.AddControllers(o => o.Filters.Add<TimingFilter>()); // globalMinimal APIs have endpoint filters (.AddEndpointFilter(...)) for the same purpose.
MVC with views
The "V" in MVC is for views: controllers can return HTML rendered with Razor templates (return View(model);). For new server-rendered pages, Razor Pages or Blazor (later in this track) are usually simpler — but MVC views are common in existing apps.
Controllers or minimal APIs?
| Minimal APIs | Controllers | |
|---|---|---|
| Style | Functions mapped to routes | Classes with action methods |
| Ceremony | Low | Higher |
| Performance | Slightly faster | Very fast |
| Features | Endpoint filters, typed results, groups | Filters pipeline, model-binding extensibility, conventions, views |
| Common in | New projects, microservices | Existing and large codebases |
Both use the same routing, DI, authentication and serialization. Choose one style per project for consistency.
Key takeaways
- Controllers group actions in classes deriving from
ControllerBase, with attribute routing. [ApiController]adds automatic400responses for invalid models (DataAnnotations).- Filters add cross-cutting behavior around actions.
- Minimal APIs and controllers are both production-ready; pick one style per project.
Quiz
4 questions · 3 correct to pass1.What does the [ApiController] attribute add?
2.In `[Route("api/[controller]")]` on ProductsController, what's the base route?
3.What is an action filter useful for?
4.Should a new API use controllers or minimal APIs?
To complete this lesson
- ○ Pass the quiz (3/4 correct)