Routing is how ASP.NET Core matches incoming URLs (like GET /api/users/42) to the exact C# code (Controller and Action Method) that should process the request. Understanding how to structure your routes is crucial for building a predictable, REST-compliant API.
Defined globally in Program.cs (e.g., pattern: "{controller}/{action}/{id?}"). Primarily used in traditional MVC UI applications. Anti-Pattern for Web APIs.
Defined locally using C# Attributes directly above the controllers and action methods. This is the industry standard for Web APIs because it provides fine-grained, explicit control over HTTP semantics.
Attribute Routing empowers you to strongly type your URL routes using constraints. If a client sends string data to an endpoint expecting an integer, the route rejects the request instantly with a 404 Not Found before your code even executes.
[ApiController]
// [controller] is a token. It automatically outputs "api/products"
// by stripping the word "Controller" from the class name.
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// 1. Basic Route: Matches GET /api/products
[HttpGet]
public IActionResult GetAll() { ... }
// 2. Route Parameter: Matches GET /api/products/42
// Added a Route Constraint ":int" - enforces the ID MUST be an integer.
// Added ":min(1)" - enforces ID cannot be 0 or negative.
[HttpGet("{id:int:min(1)}")]
public IActionResult GetById(int id)
{
return Ok($"Found product {id}");
}
// 3. Optional Parameters & Defaults: Matches GET /api/products/category/books?page=1
// A "?" makes a parameter optional, but setting a default value is better.
[HttpGet("category/{categoryName=General}")]
public IActionResult GetByCategory(string categoryName) { ... }
}
The most common startup crash in ASP.NET Core APIs is an AmbiguousMatchException. This occurs when two methods try to listen to the exact same URL and HTTP Verb pattern.
// ❌ BAD: DANGEROUS CODE THAT CRASHES APP ON START
[HttpGet]
public IActionResult GetActiveUsers() { ... }
[HttpGet]
public IActionResult GetInactiveUsers() { ... }
// Both methods match "GET /api/users". The framework panics.
// ✅ GOOD: DIVERGING THE PATHS
[HttpGet("active")] // Matches GET /api/users/active
public IActionResult GetActiveUsers() { ... }
[HttpGet("inactive")] // Matches GET /api/users/inactive
public IActionResult GetInactiveUsers() { ... }
When you create a robust API, you often need to generate a URL pointing to a newly created resource. To do this, you must explicitly Name your routes using the Name = "" parameter.
[HttpPost]
public IActionResult CreateProduct(ProductDto dto)
{
var newProduct = _db.SaveToDatabase(dto);
// Creates a 201 Created HTTP Response, AND generates a "Location" header URL
// pointing back to the "GetProductById" action.
return CreatedAtRoute("GetProductById", new { id = newProduct.Id }, newProduct);
}
// We explicitly named this route so the POST method above can find it globally
[HttpGet("{id}", Name = "GetProductById")]
public IActionResult GetProduct(int id) { ... }
Q: "Why is Attribute Routing heavily preferred over Conventional Routing when building RESTful APIs in ASP.NET Core?"
Architect Answer: "RESTful principles dictate that URLs should represent *Resources*, not internal code structures. Conventional Routing forces the URL to mirror the specific Controller and Action method name (e.g., /api/Users/GetActiveUsers). This violates REST standards because 'GetActiveUsers' is an action verb. Attribute routing completely decouples the URL structure from the C# method names. We can name our method GetActiveUsers internally for C# readability, but use [HttpGet('active')] above it, making the resulting URL a clean GET /api/users/active. Furthermore, Attribute Routing natively integrates HTTP verb constraints (GET, POST, PUT), which is foundational to API design."