As an MVC application grows, a flat Controllers folder with thirty files stops scaling — the admin screens, the customer store, and the API endpoints all blur together. Areas are ASP.NET Core MVC's built-in answer: self-contained sections of the app, each with its own controllers, views, and URL prefix, without spinning up separate projects.

This guide sets up an Admin area on .NET 10 — folder structure, routing, link generation, and the sharp edges — verified against the companion repository.
What an area gives you
- A URL namespace: everything under
/Admin/...routes to the area's controllers. - A folder namespace:
Areas/Admin/ControllersandAreas/Admin/Viewskeep the section's code together. - Independent view resolution: the area's views can differ completely from the main app's, while still sharing layouts when you want.
Typical splits: Admin, Account, Api, or per-business-domain sections (Billing, Catalog) in a modular monolith.
Folder structure
AspNetCoreMvcExamples/
├── Areas/
│ └── Admin/
│ ├── Controllers/
│ │ └── DashboardController.cs
│ └── Views/
│ └── Dashboard/
│ └── Index.cshtml
├── Controllers/
├── Views/
└── Program.cs
Only the Areas/{name}/Controllers and Areas/{name}/Views conventions matter; Models can live wherever your architecture puts them.
The controller: [Area] is mandatory
[Area("Admin")]
public class DashboardController : Controller
{
public IActionResult Index()
{
ViewBag.Stats = new Dictionary<string, int>
{
["Products"] = Catalog.Products.Count,
["Out of stock"] = Catalog.Products.Count(p => p.Stock == 0),
};
return View();
}
}
The [Area("Admin")] attribute is what binds the controller to the area — the folder location alone does nothing. Forgetting the attribute is the classic areas bug: the controller still exists, but it matches the default route, and its views resolve from the wrong folder.
Routing
Register an area route before the default route, using the {area:exists} constraint:
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
{area:exists} only matches URL prefixes that correspond to a registered area, so /Products still falls through to the default route while /Admin/Dashboard hits the area. Verified:
GET /Admin/Dashboard → 200 (Areas/Admin/Views/Dashboard/Index.cshtml)
GET /Products → 200 (Views/Products/Index.cshtml)
Attribute routing works too, and larger apps often prefer it: [Route("admin/[controller]/[action]")] on a base AdminController gives every admin controller the prefix without pattern juggling.
View resolution and layouts
For an area request, Razor searches Areas/Admin/Views/{controller}/, then Areas/Admin/Views/Shared/, then the app-level Views/Shared/. That fallback is exactly what makes layout sharing easy — the area view can opt into the main layout explicitly:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewData["Title"] = "Admin dashboard";
}
If the area needs its own chrome (a sidebar-heavy admin shell), drop a _ViewStart.cshtml and _Layout.cshtml into Areas/Admin/Views/ and the whole area switches without touching the public site. Remember to add a _ViewImports.cshtml in the area (or rely on the root one) so tag helpers work.
Generating links across areas
Tag helpers need to be told about area boundaries — this is the second classic bug. From an area view, a link back to the main app must set asp-area="":
<a asp-area="" asp-controller="Products" asp-action="Index">Back to the store</a>
And from the main app into the area:
<a asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Admin</a>
Omit asp-area and the current request's area is assumed — links inside the admin area silently point at /Admin/Products instead of /Products, which 404s. The same applies to RedirectToAction: pass new { area = "" } when crossing out of an area.
Securing an area
Areas are an organizational feature, not a security boundary — /Admin/... URLs are as public as any other until you say otherwise. The clean approach is an authorization policy applied to the whole area:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Dashboard}/{action=Index}/{id?}")
.RequireAuthorization("AdminOnly");
or [Authorize(Policy = "AdminOnly")] on a shared base controller for the area. Either way, the rule lives in one place instead of on thirty actions.
Areas vs. alternatives
- Feature folders (organizing by feature instead of by controller/view kind) solve code organization without URL prefixes — some teams combine both.
- Separate projects / Razor class libraries are the step up when a section needs independent deployment or reuse across apps.
- Minimal API groups (
MapGroup("/admin")) are the equivalent concept for endpoint-style APIs.
Areas hit the sweet spot when one deployable app has a few clearly distinct sections with their own screens.
Summary
Create Areas/{Name}/Controllers and Views, mark controllers with [Area], register the {area:exists} route before the default, and be explicit with asp-area on every cross-boundary link. Add an authorization policy at the area edge, and you have a tidy admin section inside one deployable app. The complete working area is in the companion repository.
Comments (0)
No comments yet — be the first to share your thoughts.