Attribute Routing in ASP.NET MVC 5

Routing is how ASP.NET MVC matches a URI to an action. MVC 5 supports a new type of routing, called attribute routing. As the name implies, attribute routing uses attributes to define routes. Attribute routing gives you more control over the URIs in your web application.

The earlier style of routing, called convention-based routing, is still fully supported. In fact, you can combine both techniques in the same project.

This post will cover the basic features and options of Attribute Routing, in ASP.NET MVC 5.

(Don’t mind the specific syntax right now, we will touch on this later.)

In previous version of ASP.NET MVC, the rules would be set in the RouteConfig.cs file, and point to the actual controller actions, as such:

routes.MapRoute(

name: “ProductPage”,

url: “{productId}/{productTitle}”,

defaults: new { controller = “Products”, action = “Show” },

constraints: new { productId = “\\d+” }

);

When the route definitions are co-located with the actions, within the same source file rather than being declared on an external configuration class, it can make it easier to reason about the mapping between URIs and actions. The previous route definition would be set using the following, simple attribute:

Optional URI Parameters and Default Values

You can make a URI parameter optional by adding a question mark to the route parameter. You can also specify a default value by using the form parameter=value.

public class BooksController : Controller

{

// eg: /books

// eg: /books/1430210079

[Route(“books/{isbn?}”)]

public ActionResult View(string isbn)

{

if (!String.IsNullOrEmpty(isbn))

{

return View(“OneBook”, GetBook(isbn));

}

return View(“AllBooks”, GetBooks());

}

// eg: /books/lang

// eg: /books/lang/en

// eg: /books/lang/he

[Route(“books/lang/{lang=en}”)]

public ActionResult ViewByLanguage(string lang)

{

return View(“OneBook”, GetBooksByLanguage(lang));

}

}

In this example, both /books and /books/1430210079 will route to the “View” action, the former will result with listing all books, and the latter will list the specific book. Both /books/lang and /books/lang/en will be treated the same.

Route Prefixes

Often, the routes in a controller all start with the same prefix. For example:

public class ReviewsController : Controller

{

// eg: /reviews

[Route(“reviews”)]

public ActionResult Index() { … }

// eg: /reviews/5

[Route(“reviews/{reviewId}”)]

public ActionResult Show(int reviewId) { … }

// eg: /reviews/5/edit

[Route(“reviews/{reviewId}/edit”)]

public ActionResult Edit(int reviewId) { … }

}

You can set a common prefix for an entire controller by using the [RoutePrefix] attribute:

[RoutePrefix(“reviews”)]

public class ReviewsController : Controller

{

// eg.: /reviews

[Route]

public ActionResult Index() { … }

// eg.: /reviews/5

[Route(“{reviewId}”)]

public ActionResult Show(int reviewId) { … }

// eg.: /reviews/5/edit

[Route(“{reviewId}/edit”)]

public ActionResult Edit(int reviewId) { … }

}

Use a tilde (~) on the method attribute to override the route prefix if needed:

[RoutePrefix(“reviews”)]

public class ReviewsController : Controller

{

// eg.: /spotlight-review

[Route(“~/spotlight-review”)]

public ActionResult ShowSpotlight() { … }

…

}

Default Route

You can also apply the [Route] attribute on the controller level, capturing the action as a parameter. That route would then be applied on all actions in the controller, unless a specific [Route] has been defined on a specific action, overriding the default set on the controller.

[RoutePrefix(“promotions”)]

[Route(“{action=index}”)]

public class ReviewsController : Controller

{

// eg.: /promotions

public ActionResult Index() { … }

// eg.: /promotions/archive

public ActionResult Archive() { … }

// eg.: /promotions/new

public ActionResult New() { … }

// eg.: /promotions/edit/5

[Route(“edit/{promoId:int}”)]

public ActionResult Edit(int promoId) { … }

}

Route Constraints

Route constraints let you restrict how the parameters in the route template are matched. The general syntax is {parameter:constraint}. For example:

// eg: /users/5

[Route(“users/{id:int}”]

public ActionResult GetUserById(int id) { … }

// eg: users/ken

[Route(“users/{name}”]

public ActionResult GetUserByName(string name) { … }

Here, the first route will only be selected if the “id” segment of the URI is an integer. Otherwise, the second route will be chosen.

The following table lists the constraints that are supported.

Constraint

Description

Example

alpha

Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z)

{x:alpha}

bool

Matches a Boolean value.

{x:bool}

datetime

Matches a DateTime value.

{x:datetime}

decimal

Matches a decimal value.

{x:decimal}

double

Matches a 64-bit floating-point value.

{x:double}

float

Matches a 32-bit floating-point value.

{x:float}

guid

Matches a GUID value.

{x:guid}

int

Matches a 32-bit integer value.

{x:int}

length

Matches a string with the specified length or within a specified range of lengths.

{x:length(6)} {x:length(1,20)}

long

Matches a 64-bit integer value.

{x:long}

max

Matches an integer with a maximum value.

{x:max(10)}

maxlength

Matches a string with a maximum length.

{x:maxlength(10)}

min

Matches an integer with a minimum value.

{x:min(10)}

minlength

Matches a string with a minimum length.

{x:minlength(10)}

range

Matches an integer within a range of values.

{x:range(10,50)}

regex

Matches a regular expression.

{x:regex(^\d{3}-\d{3}-\d{4}$)}

Notice that some of the constraints, such as “min”, take arguments in parentheses.

You can apply multiple constraints to a parameter, separated by a colon, for example:

// eg: /users/5// but not /users/10000000000 because it is larger than int.MaxValue,// and not /users/0 because of the min(1) constraint.

[Route(“users/{id:int:min(1)}”)]

public ActionResult GetUserById(int id) { … }

Specifying that a parameter is Optional (via the ‘?’ modifier) should be done after inline constraints:

// eg: /greetings/bye// and /greetings because of the Optional modifier,// but not /greetings/see-you-tomorrow because of the maxlength(3) constraint.[Route(“greetings/{message:maxlength(3)?}”)]

public ActionResult Greet(string message) { … }

Custom Route Constraints

You can create custom route constraints by implementing the IRouteConstraint interface. For example, the following constraint restricts a parameter to set of valid values:

Route Names

You can specify a name for a route, in order to easily allow URI generation for it. For example, for the following route:

[Route(“menu”, Name = “mainmenu”)]

public ActionResult MainMenu() { … }

you could generate a link using Url.RouteUrl:

<a href=”@Url.RouteUrl(“mainmenu”)“>Main menu</a>

Areas

You can define that a controller belongs to an area by using the [RouteArea] attribute. When doing so, you can safely remove the AreaRegistration class for that area.

[RouteArea(“Admin”)]

[RoutePrefix(“menu”)]

[Route(“{action}”)]

public class MenuController : Controller

{

// eg: /admin/menu/login

public ActionResult Login() { … }

// eg: /admin/menu/show-options

[Route(“show-options”)]

public ActionResult Options() { … }

// eg: /stats

[Route(“~/stats”)]

public ActionResult Stats() { … }

}

With this controller, the following link generation call will result with the string “/Admin/menu/show-options“

Url.Action(“Options”, “Menu”, new { Area = “Admin” })

You can set up a custom prefix for the area that defer from the area name, by using the AreaPrefix named parameter, for example:

[RouteArea(“BackOffice”, AreaPrefix = “back-office”)]

If you are using both Areas with route attributes, and areas with convention based routes (set by an AreaRegistration class), then you need to make sure that area registration happen after MVC attribute routes are configured, however before the default convention-based route is set. The reason is that route registration should be ordered from the most specific (attributes) through more general (area registration) to the mist generic (the default route) to avoid generic routes from “hiding” more specific routes by matching incoming requests too early in the pipeline.

When the route template has a non-optional part {lang}, then a request that comes in with the path "/books/lang" would not match the rule at all. What can be done is to set {lang?}, then the route would match.

The differences then between having the routing mechanism supply the default value (via {lang=en}) and the C# mechanism for default value (string lang = "en") are somewhat subtle. From the ASP.NET MVC runtime perspective, some advanced scenarios with custom model binding might be affected by this. From the user-code perspective, if you need the controller to always set default value for missing parameters even if not going through the routing mechanism (for example when calling the action method directly from your code, or in unit tests scenarios), you might want to prefer setting the default value on the method, rather than on the route.

I'm not 100% sure regarding your second comment – are you suggesting a clarification of the code-comment?

Seem like that LowercaseUrls still does not support PreserveCaseForUrlParameters? Can we obtain this feature without other nuget packages such as AttributeRouting or LowercaseRoutesMVC (at least for some actions)? If we use AttributeRouting, we'll have full functions and built-in functions is not needed anymore.

Also currently we do not have support for supplying per-route message handlers in Web Api's attribute routing as we did not find scenarios where users would use them along with attribute routing. However, we would love to hear any scenarios that you have where this support is necessary.

I am currently using a handler that validates the request. The long story is that user has to signin where he gets a token. User has then to pass this token in header for every method protected with authorization. For token verification I am using an instance of DelegatingHandler derived class to early accept or refuse the request.

So, the routes that require authorization are using that handler, others don't.

Sure, there are plenty of ways to implement that. AFAIK the handler gets executed early in the pipeline and thus fails early when request isn't authenticated. I know I could implement the same using an attribute, not a huge deal. I am just wondering whether there is way to use a handler with the attribute routing. Theoretically I could pass a handler type through an attribute.

Several issues here. First of all, no way to control ordering of routes. If a dev uses resharper to alphabetize the methods, then ordering is screwed up. Add an Order property and then OrderBy(). Second, MapMvcAttributeRoutes apparently does not retrieve routes defined in a base class. We share controller base classes between application rather than replicating code in each application.

Several issues here. First of all, no way to control ordering of routes. If a dev uses resharper to alphabetize the methods, then ordering is screwed up. Add an Order property and then OrderBy(). Second, MapMvcAttributeRoutes apparently does not retrieve routes defined in a base class. We share controller base classes between application rather than replicating code in each application.

Regarding issue #1 – could you please share a specific scenario in which the actual order of the actions and routes makes a difference? I'd add that even a stable method ordering does not guarantee a stable attribute ordering, so we take measures to re-order the routes that came out of attributes to make the most sense possible. If you come up with scenario that breaks the reordering logic, please open a bug for it at aspnetwebstack.codeplex.com/…/advanced

Regarding your second issue – this was intentional. The main motivator for attribute routing and a key design principle for the feature, is to have the route definitions as close as possible to the actual controller code. Perhaps you could start a discussion at aspnetwebstack.codeplex.com/…/asp-net-mvc about your scenario, and we could think of ways to make it work.

May seem a stupid comment but what do you have to reference to get [Route("")] to compile. I have an existing MVC project which I updated to 5 and i can do [HttpGet("")] but get the red squigglys when I try to do [Route("")]?

@Code Uniquely: The HttpGetAttribute should not have a constructor that accept a string in the RTM version of MVC 5, so [HttpGet("")] should not be compiling for you. [Route] should. Could you please verify that you indeed are using the RTM version of MVC 5?

How would you use this in a multilingual website that supports english, arabich and russian. I mean how would you get the respective language codes; en, ar, ru into the url. Say for example the user is from russia and has browser setting set to russian, I want to use attribute routing in such a way that all routes are prefixed with ru/

As a side note, if this is for a public website, I would expect that there would be a single canonical URL to represent that page, and either have one of the URLs response redirect to the other, or have the html response include a <link rel="canonical" href=… > so that it would not be considered a Duplicate Content by search engines.

Does MVC 5 support subdomain routing? I've been using blog.maartenballiauw.be/…/ASPNET-MVC-Domain-Routing.aspx this solution with my MVC 4 project and wondered if MVC 5's routing would be able to do something like this.

A lot of content is missing at the end! Seems like the formatting has gone awry a bit after “Route Constraints”. There is nothing concerning Areas anymore, for instance, and the constraints table is empty. Unfortunately the WayBack machine does not have a copy …