Controllers vs Minimal APIs
15 questions found
What is the fundamental structural difference between controller-based APIs (MVC) and minimal APIs in ASP.NET Core?
Beginner
Controller-based APIs organize endpoints as methods within controller classes (inheriting ControllerBase), using attributes ([HttpGet], [Route]) to define routes, relying on the MVC framework's action invocation pipeline. Minimal APIs define endpoints as direct lambda or method group registrations via app.MapGet/MapPost/etc. directly in Program.cs (or organized into extension methods), without requiring a full controller class, filters pipeline, or the MVC framework's heavier abstractions.
// Controller-based
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase {
[HttpGet("{id}")]
public IActionResult Get(int id) => Ok(_service.GetById(id));
}
// Minimal API equivalent
app.MapGet("/api/products/{id}", (int id, IProductService service) => service.GetById(id));
Real-world example
A team building a small, focused microservice with only five endpoints chooses minimal APIs for their reduced ceremony, while a large, complex API with dozens of related endpoints and shared cross-cutting filter logic sticks with controllers.
Common follow-ups: What performance difference exists between the two approaches?;Can you mix both controllers and minimal APIs in the same project?
Endpoint Metadata
Route Constraints & Templates;Filters
What features do controllers provide out of the box that minimal APIs don't have native equivalents for?
Intermediate
Controllers provide built-in support for action filters/exception filters/result filters (a rich, multi-stage filter pipeline), automatic model validation via [ApiController]'s ModelState checking, convention-based routing, and view rendering (for MVC views/Razor Pages) -- minimal APIs instead use the lighter-weight IEndpointFilter for cross-cutting concerns and require more explicit, manual validation logic, reflecting a deliberate design trade-off favoring simplicity and lower overhead over built-in richness.
// Controllers: automatic model validation via [ApiController]
[ApiController]
public class ProductsController : ControllerBase {
[HttpPost]
public IActionResult Create(ProductDto dto) {
// ModelState.IsValid already checked automatically, 400 returned if invalid
return Ok(_service.Create(dto));
}
}
// Minimal API: validation typically requires explicit logic or a library like FluentValidation
Real-world example
A team migrating a validation-heavy controller-based API to minimal APIs discovers they need to add explicit validation middleware or a library like MinimalApis.Extensions to replicate the automatic ModelState validation they previously got for free with [ApiController].
Common follow-ups: How does IEndpointFilter provide similar functionality to MVC's filter pipeline?;What third-party libraries fill the validation gap for minimal APIs?
Model Binding & Validation;Filters
How does the underlying request execution pipeline differ in performance between controllers and minimal APIs, and what accounts for the difference?
Advanced
Minimal APIs generally have lower per-request overhead since they bypass much of the MVC framework's action invocation machinery (the filter pipeline resolution, model binding via ModelState, action selection reflection) that controllers go through -- benchmarks typically show minimal APIs achieving somewhat higher requests-per-second and lower latency for simple endpoints, though for complex endpoints with heavy business logic, the framework overhead difference becomes a smaller fraction of total request time and matters less in practice.
// Minimal APIs: more direct execution path
app.MapGet("/ping", () => "pong"); // minimal framework overhead
// Controllers: more machinery invoked (filters, model binding, action selection)
[HttpGet("ping")]
public IActionResult Ping() => Ok("pong");
Real-world example
A high-throughput, latency-sensitive gateway service migrates its simplest pass-through endpoints from controllers to minimal APIs after benchmarking showed a measurable throughput improvement, while keeping complex business-logic-heavy endpoints on controllers where the overhead difference was negligible.
Common follow-ups: At what request complexity does the framework overhead difference become negligible?;What specific MVC pipeline stages does a minimal API bypass?
Diagnostics & Performance;Endpoint Metadata
Route Constraints & Templates
How do you organize a large number of minimal API endpoints without ending up with an enormous, unwieldy Program.cs file?
Intermediate
Common patterns include extension methods grouping related endpoint registrations (e.g., MapProductEndpoints(this WebApplication app)) called once from Program.cs, or the newer RouteGroupBuilder (via app.MapGroup("/products")) letting you apply shared prefixes, filters, and metadata to a cohesive group of related endpoints defined in a separate file -- both approaches keep Program.cs concise while maintaining minimal API's lightweight style.
// ProductEndpoints.cs
public static class ProductEndpoints {
public static void MapProductEndpoints(this WebApplication app) {
var group = app.MapGroup("/api/products").RequireAuthorization();
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
}
}
// Program.cs
app.MapProductEndpoints();
Real-world example
A minimal-API-based project with 60+ endpoints organizes them into a dozen feature-specific extension method files (MapProductEndpoints, MapOrderEndpoints, etc.), keeping Program.cs to a handful of clean, readable registration calls.
Common follow-ups: How does MapGroup let you apply shared authorization or filters to an entire group at once?;What naming/organizational conventions work best for large minimal API codebases?
Endpoint Metadata
Route Constraints & Templates;Authorization
How would you decide between controllers and minimal APIs for a new project, weighing team familiarity, project complexity, and long-term maintainability?
Advanced
Consider: team familiarity (existing MVC expertise favors controllers, reducing onboarding friction), project complexity (many cross-cutting concerns like complex filters, view rendering, or extensive convention-based routing favor controllers' richer built-in tooling), performance sensitivity (minimal APIs for latency-critical, high-throughput services), and codebase size (minimal APIs' lower ceremony suits smaller, focused services well but can become unwieldy without discipline at very large scale) -- there's no universally correct choice, and many organizations use both across different services based on each service's specific needs.
// Decision framework applied:
// - Public-facing, complex e-commerce API with dozens of related resources, filters, views -> Controllers
// - Internal, narrow, high-throughput pricing microservice with 5 endpoints -> Minimal APIs
// - Team deeply experienced with MVC, tight deadline -> Controllers (familiarity wins)
Real-world example
An organization standardizes on minimal APIs for new, narrowly-scoped microservices while maintaining existing large MVC-based applications as controllers, pragmatically choosing per-project rather than mandating one approach organization-wide.
Common follow-ups: What signals suggest a minimal API project has grown complex enough to benefit from migrating to controllers?;How do teams that use both approaches keep conventions consistent across services?
Endpoint Metadata
Route Constraints & Templates;Testing ASP.NET Core Applications
How does model binding work differently between controllers (using [FromBody], [FromQuery] attributes) and minimal APIs (using parameter inference)?
Intermediate
Controllers require explicit binding source attributes ([FromBody], [FromQuery], [FromRoute]) in ambiguous cases, though [ApiController] provides some automatic inference. Minimal APIs infer binding sources more aggressively by default based on parameter type and route pattern matching (a parameter matching a route template segment binds from the route, a complex type typically binds from the body), reducing the need for explicit attributes in common cases while still supporting them for disambiguation when needed.
// Minimal API: automatic inference based on parameter characteristics
app.MapPost("/products/{id}", (int id, ProductDto dto, [FromServices] IProductService service) => {
// id inferred from route, dto inferred from body, service explicitly from DI
return service.Update(id, dto);
});
Real-world example
A minimal API endpoint binds its route parameter, request body, and injected service all through inference and one explicit [FromServices] attribute, achieving the same result as a controller action with more verbose explicit attributes on every parameter.
Common follow-ups: When does minimal API's automatic inference get it wrong, requiring explicit attributes?;How does model binding performance compare between the two approaches?
Model Binding & Validation;Dependency Injection
How do you implement equivalent cross-cutting concerns (like automatic model validation) in minimal APIs that [ApiController] provides automatically for controllers?
Advanced
Since minimal APIs lack [ApiController]'s automatic ModelState validation, you implement it via a custom IEndpointFilter that validates the incoming DTO (often using DataAnnotations' Validator class or a library like FluentValidation) before the endpoint handler runs, returning a 400 response with validation errors if invalid -- applied globally to a route group via .AddEndpointFilter() rather than needing per-action boilerplate.
public class ValidationFilter<T> : IEndpointFilter {
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) {
var dto = context.GetArgument<T>(0);
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateObject(dto, new ValidationContext(dto), validationResults, true))
return Results.ValidationProblem(validationResults.ToDictionary(...));
return await next(context);
}
}
app.MapPost("/products", Create).AddEndpointFilter<ValidationFilter<ProductDto>>();
Real-world example
A team building a large minimal-API-based project writes one reusable generic ValidationFilter<T> applied to every endpoint accepting a DTO, replicating [ApiController]'s automatic validation behavior consistently without per-endpoint duplication.
Common follow-ups: How does this compare in complexity/maintainability to just using controllers with [ApiController]?;What other MVC conveniences commonly need to be reimplemented for minimal APIs?
Model Binding & Validation;Filters
Can a single ASP.NET Core project use both controllers and minimal APIs simultaneously, and what are valid reasons to do so?
Intermediate
Yes -- app.MapControllers() and individual app.MapGet/MapPost calls can coexist in the same Program.cs without conflict, as long as their route patterns don't overlap ambiguously. Valid reasons include incrementally migrating a controller-based application to minimal APIs feature-by-feature, or using minimal APIs specifically for a small number of simple, high-throughput endpoints while keeping complex, filter-heavy business logic on controllers within the same overall application.
var app = builder.Build();
app.MapControllers(); // existing, complex business logic endpoints
app.MapGet("/health", () => Results.Ok("healthy")); // simple, high-frequency endpoint as minimal API
app.MapGet("/version", () => Results.Ok(new { version = "1.4.2" }));
Real-world example
A large e-commerce API keeps its complex order-processing logic on controllers (benefiting from the filter pipeline) while adding new, simple, frequently-polled health and status endpoints as minimal APIs for their lower overhead.
Common follow-ups: What routing conflicts can arise when mixing both approaches in the same URL space?;Is this hybrid approach considered a maintainability risk long-term?
Health Checks;Endpoint Metadata
Route Constraints & Templates
How does dependency injection differ in practice between controllers (constructor injection) and minimal APIs (parameter injection)?
Advanced
Controllers exclusively use constructor injection, resolving all dependencies once when the controller instance is created per-request. Minimal APIs support injecting services directly as handler delegate parameters (automatically inferred as [FromServices] for registered service types not matching route/body binding patterns), letting each individual endpoint declare exactly the specific dependencies it needs without a shared constructor across multiple actions, which can reduce unnecessary coupling when different endpoints in the same feature area need different subsets of dependencies.
// Controller: all actions share the same constructor-injected dependencies
public class ProductsController(IProductService service, ILogger<ProductsController> logger) : ControllerBase {
[HttpGet] public IActionResult GetAll() => Ok(service.GetAll()); // has logger even if unused here
}
// Minimal API: each endpoint declares only what it actually needs
app.MapGet("/products", (IProductService service) => service.GetAll()); // no unused logger dependency
app.MapPost("/products", (IProductService service, ILogger<Program> logger, ProductDto dto) => { ... });
Real-world example
A minimal API's simple GET endpoint declares only IProductService as a dependency, while its more complex POST endpoint additionally declares ILogger, avoiding the controller pattern's tendency to inject dependencies into a shared constructor that not every action actually uses.
Common follow-ups: Does this parameter-based injection have any measurable performance difference from constructor injection?;How does this affect testability compared to controller-based DI?
Dependency Injection;Testing ASP.NET Core Applications
How do you apply the same authorization, CORS, and rate limiting policies consistently across both controllers and minimal API endpoints in a hybrid application?
Intermediate
Since both routing systems ultimately register into the same underlying ASP.NET Core endpoint routing system, policies configured centrally (via [Authorize]/RequireAuthorization, UseCors/RequireCors, and rate limiting policies) apply consistently whether attached via attributes (controllers) or fluent extension methods (minimal APIs) -- the underlying policy definitions (in AddAuthorization, AddCors, AddRateLimiter) are shared and referenced by name from either style.
builder.Services.AddAuthorization(options => options.AddPolicy("AdminOnly", p => p.RequireRole("Admin")));
// Controller usage
[Authorize(Policy = "AdminOnly")]
public class AdminController : ControllerBase { }
// Minimal API usage of the SAME policy
app.MapDelete("/admin/users/{id}", DeleteUser).RequireAuthorization("AdminOnly");
Real-world example
A hybrid application defines its 'AdminOnly' authorization policy once, applying it consistently to both a legacy AdminController (via attribute) and new minimal API admin endpoints (via RequireAuthorization), avoiding policy definition duplication across the two styles.
Common follow-ups: Are there any policies that behave subtly differently between the two invocation styles?;How would you audit a hybrid codebase to ensure policy consistency across both styles?
Authorization;Rate Limiting