The MVC request, explained
Follow a request through routing, a controller, a model, and a Razor view.
Here's what you'll be able to do.
- Explain what each part of MVC owns
- Trace a request from URL to HTML
- Recognize the difference between an action result and a response
Complete the setup lesson and run TaskBoard. Open Program.cs, HomeController.cs, and Views/Home/Index.cshtml.
Understand the idea
MVC is a way to divide responsibilities, not a rule that every application must have exactly three folders. A controller coordinates a use case. A model describes the data involved. A view renders the result. The HTTP pipeline surrounds all of this with services such as authentication, error handling, and logging.
A useful debugging habit is to trace one request end to end. If the wrong page appears, find the selected action before editing a view. If the right page contains the wrong data, inspect the model passed to that view.
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).
Match a URL to an action
TaskBoard registers a conventional route with defaults. A request to / uses HomeController.Index. A request to /Tasks uses TasksController.Index. A third segment can bind an id parameter. Routing identifies the endpoint; it does not yet produce HTML.
Authentication and authorization run before the selected action executes. Because TasksController requires authorization, an anonymous browser visiting /Tasks is challenged and sent to the login page. This is expected pipeline behavior, not a missing route.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute("default",
"{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();Let the controller coordinate
HomeController.Index is intentionally small. Returning View() creates an action result. MVC later executes that result, locates the Razor template, renders it, and writes the HTML response. A data-backed action usually retrieves data first and passes a model to View(model).
Keep rendering decisions in the view and reusable business logic in services when that separation helps. A controller should make the use case readable: obtain the current user, query their data, handle absence, and select the result.
using Microsoft.AspNetCore.Mvc;
namespace TaskBoard.Controllers;
public class HomeController : Controller
{
public IActionResult Index() => View();
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error() => View("Error", HttpContext.TraceIdentifier);
}Give the page a deliberate model
TaskListPage packages the task collection with search and pagination state. The view does not need to query the database or reconstruct the total-page calculation. A typed model gives the compiler a chance to catch a renamed property.
Compare this with a dictionary of loosely typed values. Typed page models are easier to refactor because the relationship between action and view is explicit. They also make tests more meaningful: you can inspect the exact result passed to the renderer.
public sealed record TaskListPage(
IReadOnlyList<BoardTask> Tasks,
string? Query,
int Page,
int TotalPages,
int TotalCount);Render and inspect the response
The Razor template declares its model and emits ordinary HTML. The browser never runs Razor or your C# controller. It receives the resulting document. Open View Source in your browser to see that the @Model expressions have disappeared.
Use a breakpoint in TasksController.Index and inspect the tasks list after signing in. Then step to return View(...). This separates a data issue from a presentation issue: first prove the action produced the right model, then inspect the template.
@model TaskListPage
<h1>My tasks</h1>
<p>@Model.TotalCount tasks</p>
@foreach (var task in Model.Tasks)
{
<h2>@task.Title</h2>
}Make it your own.
Add a count sentence to the task list using Model.TotalCount. Explain why calling db.Tasks.Count() from inside the Razor view would be a poorer separation of responsibilities.
See the solution & reasoning
The action already calculates the total using the current user and search filter. Reuse that value in the view. A fresh database query in the view would duplicate work, make rendering depend on persistence, and risk omitting the ownership or search filters.
When something doesn't work
“The view was not found” includes the searched locations. Check the controller folder and action view name before changing routing.
A redirect to login is not a 404. Inspect the status and Location header to distinguish authorization from route matching.
Keep namespaces aligned with @model declarations. The view compiler must be able to resolve your model type.
What does return View(model) do?
That's another skill in your toolkit.
Your progress is saved in this browser. Sign in to save completion to your account.