Routing and Areas
|
This section documents ASP.NET MVC 5.3.x, ASP.NET Web API 2.2, ASP.NET Web Pages 3, OWIN/Katana, SignalR 2,
and ASP.NET Identity 2 — all running on .NET Framework 4.8.1 — the This is not ASP.NET Core MVC. ASP.NET MVC 5 is the pre-Core, This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page documents ASP.NET MVC 5.3.x routing on .NET Framework 4.8.1 — not ASP.NET Core’s endpoint routing,
which replaces RouteCollection with IEndpointRouteBuilder (see
Routing under ASP.NET Core).
RouteCollection and RouteTable
RouteTable.Routes is the single, process-wide RouteCollection that UrlRoutingModule matches every request
against (see The MVC Pattern and Request Life
Cycle). Convention routes are registered with MapRoute, in order, from RouteConfig.RegisterRoutes:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "ProductDetails",
url: "products/{id}",
defaults: new { controller = "Products", action = "Details" },
constraints: new { id = @"\d+" }); // regex constraint
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
}
-
Defaults (
UrlParameter.Optional) make a segment optional; a URL missing that segment still matches. -
Constraints accept either a regex
stringper segment or a customIRouteConstraint. -
IgnoreRoutemarks URLs (e.g.*.axdhandler paths) that routing should never intercept. -
Order matters:
RouteCollectiontries routes in registration order and stops at the first match — a more specific route (likeProductDetailsabove) must be registered before a catch-allDefaultroute or it is never reached.
A custom constraint implements IRouteConstraint.Match for logic a regex cannot express (e.g. checking a
database):
public class ActiveProductConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
=> int.TryParse(values[parameterName]?.ToString(), out var id) && ProductStore.IsActive(id);
}
Attribute routing
MapMvcAttributeRoutes() (called once, typically before the convention routes) enables [Route]/[RoutePrefix]
on controllers and actions, matched before convention routes in the pipeline regardless of registration
position:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
[RoutePrefix("products")]
public class ProductsController : Controller
{
[Route("")]
[Route("~/catalog")] // "~/" overrides the prefix entirely
public ActionResult Index() => View();
[Route("{id:int:min(1)}", Name = "ProductDetails")] // inline constraint + a named route
public ActionResult Details(int id) => View();
[Route("{category}/{page:int=1}")] // optional segment with a default value
public ActionResult Browse(string category, int page) => View();
}
Inline constraints (:int, :min(1), :regex(…), :alpha, …) live directly in the route template; a
Name lets Url.RouteUrl/Html.RouteLink generate URLs from that specific route regardless of which one
would otherwise match first. See
Attribute
Routing in ASP.NET MVC 5.
UrlHelper, Html.ActionLink, Url.Action
Generating URLs from route values (rather than hardcoding paths) keeps links in sync with routing changes:
string url = Url.Action("Details", "Products", new { id = 42 }); // "/products/42" or "/products/details/42"
@Html.ActionLink("View product", "Details", "Products", new { id = 42 }, null)
<a href="@Url.Action("Details", "Products", new { id = 42 })">View product</a>
Both resolve through the same RouteCollection.GetVirtualPath, so a route change is reflected everywhere links
are generated this way instead of only where URLs happen to be typed literally.
Areas
An area is a self-contained slice of an application — its own Controllers, Views, and (usually) a
distinct route namespace — registered through an AreaRegistration:
// Areas/Admin/AdminAreaRegistration.cs
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName => "Admin";
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional });
}
}
/Areas/Admin/Controllers/UsersController.cs
/Areas/Admin/Views/Users/Index.cshtml
/Areas/Admin/Views/web.config
AreaRegistration.RegisterAllAreas(), called from Application_Start before RouteConfig.RegisterRoutes,
reflects over the assembly and invokes every AreaRegistration’s `RegisterArea. Ambiguous-controller
pitfall: if both /Controllers/UsersController.cs and /Areas/Admin/Controllers/UsersController.cs exist,
DefaultControllerFactory can throw "multiple types were found that match the controller named 'Users'"
unless the area route’s namespaces constraint (or DataTokens["Namespaces"]) disambiguates which
UsersController it means:
context.MapRoute("Admin_default", "Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "MyApp.Areas.Admin.Controllers" });
See Areas in ASP.NET MVC.
Next: Razor Syntax covers how a view actually turns into HTML.