Endpoint routing, introduced in ASP.NET Core 3.0, separates route matching from endpoint execution. UseRouting() matches incoming requests to endpoints (but does not execute them yet), allowing auth and CORS middleware in between to inspect the matched endpoint’s metadata. MapControllers(), MapGet(), and MapHealthChecks() register endpoints and execute them. Understanding endpoint routing is essential for the Web API chapters — it is the mechanism through which controller actions, minimal API handlers, and health endpoints are connected to URLs.
MapControllers and Attribute Routing
// ── MapControllers — maps all [Route] and [HttpGet/Post/...] controllers ──
app.MapControllers();
// Controller endpoints are discovered via reflection at startup:
// [ApiController]
// [Route("api/posts")]
// public class PostsController : ControllerBase
// {
// [HttpGet] → GET /api/posts
// [HttpGet("{id:int}")] → GET /api/posts/42
// [HttpPost] → POST /api/posts
// [HttpPut("{id:int}")] → PUT /api/posts/42
// [HttpDelete("{id:int}")] → DELETE /api/posts/42
// }
MapControllers() is not the same as the older UseEndpoints(e => e.MapControllers()). In .NET 6+, MapControllers() can be called directly on WebApplication — the UseEndpoints wrapper is no longer needed. The UseRouting() call is also implicit when you call MapControllers() or any other Map* method — you do not need to explicitly call UseRouting() in .NET 6+ unless you need to place authentication or CORS middleware between routing and endpoint execution (which you usually do). Explicitly calling UseRouting() before auth middleware is still best practice for clarity.app.MapControllers().RequireAuthorization() applies the default authorization policy to ALL controller endpoints — useful as a secure-by-default baseline. Individual endpoints can then opt out with [AllowAnonymous]. This is safer than relying on developers to remember to add [Authorize] to every controller — you secure all endpoints by default and explicitly allow anonymous access where needed.{id:int}, {slug:regex(^[a-z-]+$)}) are validated during routing. A request to GET /api/posts/abc for a route [HttpGet("{id:int}")] returns 404 (no matching route), not 400. Design your routes with this in mind — if you need a 400 Bad Request for invalid input format, use model binding validation rather than route constraints. Route constraints are for disambiguation, not input validation.Minimal API Endpoints
// ── Minimal API endpoints — without controllers ───────────────────────────
// Useful for simple endpoints, webhooks, or prototyping
// Group related endpoints
var posts = app.MapGroup("/api/posts")
.RequireAuthorization() // all posts endpoints require auth
.WithTags("Posts") // OpenAPI grouping
.WithOpenApi(); // include in Swagger
posts.MapGet("/", async (IPostService svc, CancellationToken ct) =>
Results.Ok(await svc.GetAllAsync(ct)));
posts.MapGet("/{id:int}", async (int id, IPostService svc, CancellationToken ct) =>
{
var post = await svc.GetByIdAsync(id, ct);
return Results.Ok(post);
});
posts.MapPost("/", async (CreatePostRequest req, IPostService svc, CancellationToken ct) =>
{
var post = await svc.CreateAsync(req, ct);
return Results.CreatedAtRoute("GetPostById", new { post.Id }, post);
}).WithName("CreatePost");
// ── Health checks ──────────────────────────────────────────────────────────
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("database")
.AddUrlGroup(new Uri("https://api.sendgrid.com"), "email-provider");
app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse });
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = hc => hc.Tags.Contains("ready") });
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false }); // liveness: just alive
Applying Middleware to Specific Endpoints
// ── Endpoint-level auth and CORS policy ───────────────────────────────────
app.MapControllers()
.RequireAuthorization(); // ALL controllers require default auth
app.MapHealthChecks("/health")
.AllowAnonymous() // health endpoint: no auth required
.RequireCors("HealthChecks"); // CORS: only allow internal monitoring
// ── Endpoint-level rate limiting (.NET 7+) ────────────────────────────────
app.MapPost("/api/auth/login")
.RequireRateLimiting("loginPolicy"); // strict rate limit on login
Common Mistakes
Mistake 1 — Relying on developers to add [Authorize] to every controller
❌ Wrong — one forgotten [Authorize] attribute exposes the endpoint to unauthenticated users.
✅ Correct — use MapControllers().RequireAuthorization() for secure-by-default; opt out with [AllowAnonymous] where needed.
Mistake 2 — Using route constraints for input validation (returns 404 not 400)
❌ Wrong — invalid format returns 404 (no matching route), confusing clients.
✅ Correct — use loose route templates and validate in model binding/FluentValidation to return 400 with a descriptive error.