Split a growing MVC app into areas such as Admin and Customer, each with its own controllers, views and layout, register area routes, generate links between areas with asp-area, and keep shared views working.

Bishal Biswas
Self Learner
When one Controllers folder is not enough
A shop has a public storefront and an admin back office. Both have a ProductsController. The storefront one shows products to customers. The admin one lets staff edit them. With a single Controllers folder, the two collide, and the Views folder becomes a mess of unrelated pages.
Areas solve this. An area is a self contained section of the app with its own controllers, views and models folders. Areas/Admin/Controllers/ProductsController and Controllers/ProductsController can live side by side, and their URLs are different: /Admin/Products and /Products.
The folder structure
Areas/
Admin/
Controllers/
DashboardController.cs
ProductsController.cs
Views/
Dashboard/
Index.cshtml
Products/
Index.cshtml
Edit.cshtml
Shared/
_Layout.cshtml
_ViewImports.cshtml
_ViewStart.cshtml
Models/
Controllers/
Views/
The area folder name (Admin) is the area name. Inside, the layout is exactly what you already know: a Controllers folder and a Views folder with one subfolder per controller.
Marking the controller
A controller belongs to an area only if it says so with the [Area] attribute:
using Microsoft.AspNetCore.Mvc;
namespace ShopApp.Areas.Admin.Controllers;
[Area("Admin")]
public class ProductsController : Controller
{
public IActionResult Index()
{
return View();
}
}
The folder alone is not enough. Without the attribute, routing will not know the controller is in an area, and the view engine will look in the wrong place.
Registering the route
Area URLs need a route pattern with an {area} segment. Add it before the default route in Program.cs:
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
{area:exists} is a constraint that only matches if a registered area has that name. A request to /Admin/Products matches the first route. A request to /Products does not, because there is no area called Products, so it falls through to the default route.
If you use attribute routing, put the area in the route template:
[Area("Admin")]
[Route("admin/products")]
public class ProductsController : Controller
{
[HttpGet("")]
public IActionResult Index() => View();
[HttpGet("{id:int}/edit")]
public IActionResult Edit(int id) => View();
}
Where views are found
For a controller in an area, the view engine looks in:
Areas/Admin/Views/Products/Index.cshtml
Areas/Admin/Views/Shared/Index.cshtml
Views/Shared/Index.cshtml
The last line matters. Partials, layouts and error pages in the root Views/Shared folder are still found from inside an area. You do not need to copy _ValidationScriptsPartial into every area.
_ViewImports and _ViewStart
These two files are per folder tree, so an area needs its own copies:
@* Areas/Admin/Views/_ViewImports.cshtml *@
@using ShopApp
@using ShopApp.Areas.Admin.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@* Areas/Admin/Views/_ViewStart.cshtml *@
@{
Layout = "_Layout";
}
With this _ViewStart, views in the area use Areas/Admin/Views/Shared/_Layout.cshtml if it exists, or fall back to the root Views/Shared/_Layout.cshtml. An admin section usually has its own layout with a different navigation bar, so it is common to give the area one.
To reuse the root layout on purpose, set the full path:
@{
Layout = "/Views/Shared/_Layout.cshtml";
}
Links between areas
Tag helpers take an asp-area attribute:
<a asp-area="Admin" asp-controller="Products" asp-action="Index">Manage products</a>
<a asp-area="" asp-controller="Home" asp-action="Index">Back to the store</a>
asp-area="" with an empty string means “the root, not an area”. Leave asp-area off entirely and the link stays in the current area, which is what you want for links within the admin section but not for a “back to site” link.
The same rule applies to RedirectToAction:
return RedirectToAction("Index", "Home", new { area = "" });
And to Url.Action:
var url = Url.Action("Edit", "Products", new { area = "Admin", id = 5 });
Securing an area
Areas are a natural place for authorization. Instead of decorating every controller, put the rule in one base class:
[Area("Admin")]
[Authorize(Roles = "Admin")]
public abstract class AdminControllerBase : Controller
{
}
public class ProductsController : AdminControllerBase
{
// every action requires the Admin role
}
Or apply it through a convention in Program.cs so the requirement is impossible to forget:
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new AreaAuthorizeConvention("Admin", "Admin"));
});
The role and policy topic explains the pieces.
Static files per area
Static files still come from wwwroot. Areas do not have their own. Organise with subfolders such as wwwroot/admin/css/admin.css and reference them with ~/admin/css/admin.css.
Alternatives to areas
Areas group by section of the site. Another approach, sometimes called feature folders, groups by feature: everything about products in one folder, controller and views together. That needs a custom view location expander and is less common. For most apps with a clear public side and admin side, areas are the simplest answer.
Quick recap
- An area is a section of the app with its own
Controllers,ViewsandModelsfolders underAreas/{Name}. - Mark controllers with
[Area("Name")]and add an area route with{area:exists}before the default route. - Views resolve inside the area first and fall back to the root
Views/Shared. - Give each area its own
_ViewImportsand_ViewStart, and usually its own layout. - Use
asp-areaon links andnew { area = "" }on redirects to move between areas and the root. - Areas are a good boundary for authorization, such as requiring the Admin role for everything in
Admin.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// Area route first, then the default route
app.MapControllerRoute(
name: “areas”,
pattern: “{area:exists}/{controller=Dashboard}/{action=Index}/{id?}”);
app.MapControllerRoute(
name: “default”,
pattern: “{controller=Home}/{action=Index}/{id?}”);
app.Run();
// Areas/Admin/Controllers/AdminControllerBase.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ShopApp.Areas.Admin.Controllers;
[Area(“Admin”)]
[Authorize(Roles = “Admin”)]
public abstract class AdminControllerBase : Controller
{
}
// Areas/Admin/Controllers/DashboardController.cs
public class DashboardController : AdminControllerBase
{
public IActionResult Index() => View();
}
// Areas/Admin/Controllers/ProductsController.cs
public class ProductsController : AdminControllerBase
{
private readonly AppDbContext _db;
public ProductsController(AppDbContext db) => _db = db;
// GET /Admin/Products
public async Task<IActionResult> Index() =>
View(await _db.Products.OrderBy(p => p.Name).ToListAsync());
// GET /Admin/Products/Edit/5
public async Task<IActionResult> Edit(int id)
{
var product = await _db.Products.FindAsync(id);
return product is null ? NotFound() : View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, Product input)
{
var product = await _db.Products.FindAsync(id);
if (product is null) return NotFound();
product.Name = input.Name;
product.Price = input.Price;
await _db.SaveChangesAsync();
return RedirectToAction(nameof(Index)); // stays in the Admin area
}
public IActionResult ViewOnSite(int id) =>
RedirectToAction("Details", "Products", new { area = "", id }); // leaves the area
}
// Controllers/ProductsController.cs (root, public storefront: same name, different URL)
public class ProductsController : Controller
{
// GET /Products/Details/5
public IActionResult Details(int id) => View();
}
// Areas/Admin/Views/_ViewImports.cshtml
//
// @using ShopApp
// @using ShopApp.Models
// @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
// Areas/Admin/Views/_ViewStart.cshtml
//
// @{ Layout = “_Layout”; } (resolves to Areas/Admin/Views/Shared/_Layout.cshtml)
// Areas/Admin/Views/Shared/_Layout.cshtml (admin navigation)
//
// <nav>
// <a asp-area=“Admin” asp-controller=“Dashboard” asp-action=“Index”>Dashboard</a>
// <a asp-area=“Admin” asp-controller=“Products” asp-action=“Index”>Products</a>
// <a asp-area=“” asp-controller=“Home” asp-action=“Index”>Back to store</a>
// </nav>
// @RenderBody()
// Views/Shared/_Layout.cshtml (public navigation with a link into the area)
//
// @if (User.IsInRole(“Admin”))
// {
// <a asp-area=“Admin” asp-controller=“Dashboard” asp-action=“Index”>Admin</a>
// }
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready