Routes, controllers & HTTP results
Design predictable URLs and choose the right result for success, invalid input, and missing data.
Here's what you'll be able to do.
- Read conventional and attribute routes
- Use HTTP method constraints deliberately
- Return useful status codes without exposing private records
Understand the MVC request flow. Keep the TasksController and BoardApiController source open.
Understand the idea
A URL is part of your application’s public contract. Routing turns that contract into an endpoint, while an HTTP method expresses the operation. GET should retrieve a representation without changing application data. POST submits an operation that can change state.
TaskBoard uses conventional routes for pages and attribute routes for its API. Both coexist in one application. The important design choices are consistency, explicit method constraints, and a clear response when the requested resource cannot be returned.
Choose your environment
Open TaskBoard.csproj from the example download. Install the ASP.NET and web development workload and a Visual Studio release supporting .NET 10. Select the TaskBoard launch profile and press F5. Use Solution Explorer to find the files named below.
Install the .NET 10 SDK and C# Dev Kit. Open the TaskBoard folder in VS Code. Run dotnet restore, then dotnet run in the integrated terminal. Browse http://localhost:5300. Open files with Ctrl+P (Cmd+P on macOS).
Understand conventional routing
The default pattern contains a controller, action, and optional id. The URL /Tasks/Edit/12 identifies Edit on TasksController and supplies 12 for id. Query-string values such as q and page bind to matching action parameters.
Use generated links rather than assembling URLs by concatenating strings. MVC tag helpers understand route values and encode query strings correctly, which matters when searches contain spaces or punctuation.
<a asp-controller="Tasks" asp-action="Edit" asp-route-id="12">Edit task</a>
<a asp-controller="Tasks" asp-action="Index"
asp-route-q="database" asp-route-page="2">Next page</a>Constrain API routes
BoardApiController has a shared /api/tasks prefix. The integer constraint on {id:int} prevents the word summary from being treated as an id. That makes /api/tasks/summary and /api/tasks/42 unambiguous.
A route constraint selects endpoints; it is not a complete input-validation strategy. An id of -1 can still be an integer. The action must still decide whether a matching, authorized record exists.
[ApiController, Authorize, Route("api/tasks")]
public class BoardApiController(BoardDb db, IBoardSummary summary) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id, CancellationToken ct)
{
var ownerId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var task = await db.Tasks.AsNoTracking()
.Where(t => t.Id == id && t.OwnerId == ownerId)
.Select(t => new { t.Id, t.Title, t.IsComplete })
.SingleOrDefaultAsync(ct);
return task is null ? NotFound() : Ok(task);
}
}Choose a result with meaning
For a successful HTML read, return View. For successful JSON, return Ok. For a missing resource, return NotFound. After a successful form POST, RedirectToAction implements the Post/Redirect/Get pattern so refreshing the resulting page does not repeat the write.
The ownership filter is included in the query itself. A task belonging to another user and a nonexistent task both produce 404. This avoids confirming the existence of someone else’s private record. Authentication and ownership remain separate checks.
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, CancellationToken ct)
{
var task = await db.Tasks.SingleOrDefaultAsync(
t => t.Id == id && t.OwnerId == OwnerId, ct);
if (task is null) return NotFound();
db.Tasks.Remove(task);
await db.SaveChangesAsync(ct);
return RedirectToAction(nameof(Index));
}Inspect the HTTP exchange
Open the browser Network panel and create a task. Observe the POST response and the subsequent GET. Then visit an impossible API id while signed in and inspect the 404. Read the status, response headers, and response body as separate pieces of evidence.
Use the practice lab’s route explorer to see parsed controller, action, and id values. It is a deliberately bounded demonstration of the conventional route shape, not a general-purpose implementation of ASP.NET endpoint routing.
curl -i http://localhost:5300/health
# A basic liveness request returns HTTP 200 when the app is running.Make it your own.
Add a link to the second task page using tag helpers and preserve the current search query. Then inspect the generated href in the browser.
See the solution & reasoning
Use asp-action="Index", asp-route-page="2", and asp-route-q="@Model.Query". The generated query string retains the search term. A hand-written /Tasks?page=2 link would silently clear the filter.
When something doesn't work
AmbiguousMatchException usually means multiple endpoints match the same method and route. Add explicit method attributes or correct overlapping templates.
A 405 indicates that the path may exist but does not allow that HTTP method. Do not remove a POST constraint just to make a browser address-bar GET succeed.
Attribute-routed actions are not automatically reachable through every conventional URL you can invent.
Why does TaskBoard delete through POST rather than GET?
That's another skill in your toolkit.
Your progress is saved in this browser. Sign in to save completion to your account.