WORK IN PROGRESS

Attribute routing in ASP.NET MVC

One of the limitations of ASP.NET MVC's routing system is that it requires you to configure routes on a global route table. This has several consequences:
  • It forces you to encode action-specific information like parameter names in a global route table.
  • Routes registered globally can conflict with other routes and end up matching actions they aren't supposed to match.
  • The information about what URI to use to call into a controller is kept in a completely different file from the controller itself. A developer has to look at both the controller and the global route table in configuration to understand how to call into the controller.
An attribute-based approach solves all these problems by allowing you to configure how an action would be accessed right on the action itself. For most cases, this should improve usability and make Web applications simpler to build and maintain.

Usage

  1. Annotate the action with one of our HTTP verb attributes (HttpGet/HttpPost/HttpPut/HttpDelete/HttpPatch/HttpHead/HttpOptions/HttpRoute), passing in the route template in the constructor.
  2. Call the RouteCollection.MapMvcAttributeRoutes() extension method when configuring routes.
This call will use the default controller factory to locate all available actions and controllers, and retrieve route-defining attributes. It will then use these attributes to create routes and add these to the server's route collection.

This design allows attribute-based routing to compose well with the existing routing system since you can call MapMvcAttributeRoutes and still define regular routes using MapRoute. Here's an example:
routes.MapMvcAttributeRoutes();
routes.MapRoute(
  name: "Default",
  url: "{controller}/{action}/{id}",
  defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
In most cases, MapMvcAttributeRoutes will be called first so that attribute routes are registered before the global routes (and therefore get a chance to supersede global routes).

Defining simple routes

public class HomeController : Controller
{
    [HttpGet("")]
    public ActionResult Index() { ... }

    [HttpGet("about")]
    public ActionResult About() { ... }

    [HttpGet("contact-us")]
    public ActionResult ContactUs() { ... }
}

Using route parameters

Route parameters can be specified within the route template.
public class GreetingsController : Controller
{
    [HttpGet("say/hello/to/{name}")]
    public ActionResult SayHelloTo(string Name) { ... }
}

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public ActionResult GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public ActionResult GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Defining constraints over route parameters

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public ActionResult Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:

Constraint Key Description Example
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}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public ActionResult GetPerson(int id) { }
Inline constraints must appear before the optional parameter '?' and default values

The constraint resolution is extensible. See below for details.

Overloading

public class UsersController : Controller
{
    [HttpGet("show-user({id:int})", RouteName = "GetUserById")]
    public ActionResult Show(int id) { ... }

    [HttpGet("show-user({username})", RouteName = "GetUserByUsername")]
    public ActionResult Show(string username) { ... }
}

Specifying other (or no) HTTP methods on a route

public class UsersController : Controller
{
    [HttpPost("new-user")]
    public ActionResult NewUser(string name, int age) { ... }

    [HttpRoute("any-method-would-work")]
    public ActionResult WorksWithAnyMethod() { ... }
}

Specifying multiple ways to access an action

public class ProductsController : Controller
{
    [HttpGet("/p-{productId}")]
    [HttpGet("/p/{title}/{productId}")]
    public ActionResult NewUser(string productId, string title) { ... }
}

The experience for getting started with attribute-based routing will look something like this:

Route prefixes

It is possible to prefix all routes within a given controller. For example:
[RoutePrefix("users")]
public class UsersController : Controller
{
    [HttpGet("")]
    public ActionResult List() { ... }

    [HttpGet("name/{name}/id/{id}")]
    public ActionResult Get(int id, string name) { ... }
}
This will generate the following two routes:
  1. users
  2. users/name/{name}/id/{id}

Multiple RoutePrefix attributes can be specified, resulting with the Cartesian product of prefixes and routes.

For example:
[RoutePrefix("profiles")]
[RoutePrefix("users")]
public class UsersController : Controller
{
    [HttpGet("")]
    public ActionResult List() { ... }

    [HttpGet("name/{name}/id/{id}")]
    public ActionResult Get(int id, string name) { ... }
}
This will generate the following four routes:
  1. users
  2. users/name/{name}/id/{id}
  3. profiles
  4. profiles/name/{name}/id/{id}

Disabling route prefix for a specific route

In order to have a specific route bypass the prefix that is set on its controller, the following syntax would be used:
[RoutePrefix("foo")]
public class FooController : Controller
{
    [HttpGet("bar")]
    public ActionResult Bar() { ... }

    [HttpGet("~/baz")]
    public ActionResult Baz() { ... }
}
This will generate the following two routes:
  1. foo/bar
  2. baz

Areas

Routes can be mapped to an area. If a RouteAreaAttribute is applied on a controller, all of the routes on that controller would be associated with the area, and their template would be prefixed accordingly
For example:
[RouteArea("PugetSound")]
public class FooController : Controller
{
    [HttpGet("bar")]
    public ActionResult Bar() { ... }
}
The resulting route url would be PugetSound/bar.

The actual prefix applied for a given area can be tweaked by setting the AreaPrefix parameter:
[RouteArea("PugetSound", AreaPrefix = "puget-sound")]
public class FooController : Controller
{
    [HttpGet("bar")]
    public ActionResult Bar() { ... }
}
The resulting route url in that case would be puget-sound/bar.

Named routes

Route names are useful for generating links by allowing you to identify the route you want to use. You can choose to define the route name right on the attribute itself:
[HttpGet("customers/{id}", RouteName = "GetCustomerById")]

In the absence of a specified route name, MVC will generate a default route name, based on the given template and HTTP method.
For example, in the case of HttpGet("customer/{id}"), the generated name would be "Get_customer/{id}".
Since there should never be two routes with the exact same template and HTTP method specification, name collisions would not normally occur. In the case they do, it is always possible to explicitly set the RouteName on a any route.

Ordering

There is an Order property on the [RoutePrefix] attribute and a RouteOrder on the HTTP verb attributes that allows you to customize the order in which the routes are evaluated. The default order is 0, and routes with a smaller order get evaluated first. Negative numbers can be used to evaluate before the default and positive numbers can be used to evaluate after the default. In addition, a default ordering is used to order routes that don't have an order specified.

Here's how the ordering works:
  1. Compare routes by prefix order. If a prefix order is smaller, it goes earlier into the route collection. If the prefix order is the same, keep going.
  2. Compare routes by the RouteOrder on the HTTP verb attribute. If an order is smaller, it goes earlier into the route collection. If the order is the same, keep going.
  3. Go through the parsed route segment by segment. Apply the following ordering for determining which route goes first:
    1. Literal segments
    2. Constrained parameter segments
    3. Unconstrained parameter segments
    4. Constrained wildcard parameter segments
    5. Unconstrained wildcard parameter segments
  4. If no ordering can be determined up to this point, use an OrdinalIgnoreCase string comparison of the two route templates to ensure that the ordering is stable and won't change if the order of the actions and attributes changes.

Extensibility

TBD

Last edited Jun 13, 2013 at 11:07 PM by kenegozi, version 9

Comments

jrnail23 Jan 8, 2014 at 10:55 PM 
Attribute based routing is a great idea, and I was really excited to upgrade to MVC5 in order to use it. Until it was time to test my routes. Why is nearly everything that MapMvcAttributeRoutes depends on an internal class??? What happened to testability as a core value in ASP.Net MVC?