WORK IN PROGRESS
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:
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).
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:
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.
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:
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:
Inline constraints must appear before the optional parameter '?' and default values
The constraint resolution is extensible. See below for details.
The experience for getting started with attribute-based routing will look something like this:
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:
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.
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:
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:
Inline constraints must appear before the optional parameter '?' and default values, and the constraint resolution is extensible. See below for details.
The [RoutePrefix] attribute lets you define a common prefix for an entire controller. So the previous controller definition is simplified as:
When MapHttpAttributeRoutes gets called, it goes through all the prefixes and adds a route for every action. If the action has no route provider attribute, the route prefix itself gets added. If the action does have a route provider attribute, the two templates get concatenated. Multiple route prefixes will each register their own routes for each action. So if you have two route prefixes and three actions on a controller, you would have six routes generated.
Optional parameters, default values, and inline constraints can all be applied to route prefixes as well.
In the absence of a specified route name, Web API will generate a default route name. If there is only one attribute route for the action name on a particular controller, the route name will take the form "ControllerName.ActionName". If there are multiple attributes with the same action name on that controller, a suffix gets added to differentiate between the routes: "Customer.Get1", "Customer.Get2".
Here's how the ordering works:
You can pass in a custom HttpRouteBuilder to the MapHttpAttributeRoutes method:
Extending the route builder allows you to add or remove constraints, add a per-route message handler, and return a custom instance of IHttpRoute among other things.
The other extensibility point is IInlineConstraintResolver. This interface is responsible for taking an inline constraint and manufacturing an IHttpRouteConstraint for that parameter:
The inline constraint resolver is an argument to HttpRouteBuilder's constructor, so you could call the following:
The default constraint resolver uses a dictionary based approach of mapping constraint keys to a particular constraint type. It contains logic to create an instance of the type based on the constraint arguments. It exposes the dictionary publicly so that you can add custom constraints without having to implement IInlineConstraintResolver. Here's an example:
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.
Usage
- 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.
- Call the RouteCollection.MapMvcAttributeRoutes() extension method when configuring routes.
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 } );
Defining simple routes
publicclass 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.publicclass 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") { }
Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")] public ActionResult GetCountry(string name) { }
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) { }
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) { }
The constraint resolution is extensible. See below for details.
Overloading
publicclass 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
publicclass 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
publicclass 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:
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 Country GetCountry(string name = "USA") { }
Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")] public Country GetCountry(string name) { }
Inline Constraints
Route constraints can be applied to particular parameters in the route template itself. Here's an example:[HttpGet("people/{id:int}")] public Person Get(int id) { }
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 Person Get(int id) { }
Route prefixes
Frequently you’ll want to specify a common prefix for an entire controller. For example:publicclass CustomersController : ApiController { [HttpGet("customers")] public IEnumerable<Customer> Get() { } [HttpGet("customers/{id}")] public Customer Get(int id) { } [HttpPost("customers")] publicvoid Post(Customer customer) { } }
The [RoutePrefix] attribute lets you define a common prefix for an entire controller. So the previous controller definition is simplified as:
[RoutePrefix("customers")] publicclass CustomersController : ApiController { public IEnumerable<Customer> Get() { } [HttpGet("{id}")] public Customer Get(int id) { } publicvoid Post(Customer customer) { } }
When MapHttpAttributeRoutes gets called, it goes through all the prefixes and adds a route for every action. If the action has no route provider attribute, the route prefix itself gets added. If the action does have a route provider attribute, the two templates get concatenated. Multiple route prefixes will each register their own routes for each action. So if you have two route prefixes and three actions on a controller, you would have six routes generated.
Optional parameters, default values, and inline constraints can all be applied to route prefixes as well.
Naming
Web API's routing system requires every route to have a distinct name. 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, Web API will generate a default route name. If there is only one attribute route for the action name on a particular controller, the route name will take the form "ControllerName.ActionName". If there are multiple attributes with the same action name on that controller, a suffix gets added to differentiate between the routes: "Customer.Get1", "Customer.Get2".
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:
- 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.
- 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.
- Go through the parsed route segment by segment. Apply the following ordering for determining which route goes first:
- Literal segments
- Constrained parameter segments
- Unconstrained parameter segments
- Constrained wildcard parameter segments
- Unconstrained wildcard parameter segments
- 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
There are two main extensibility points that are built in - the HttpRouteBuilder and the IInlineConstraintResolver interface. HttpRouteBuilder is the class that takes a tokenized route template and creates an IHttpRoute for it. It exposes the following virtuals:publicclass HttpRouteBuilder { publicvirtual IHttpRoute BuildHttpRoute(string routeTemplate, IEnumerable<HttpMethod> httpMethods, string controllerName, string actionName); publicvirtual IHttpRoute BuildHttpRoute(HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, string routeTemplate); }
config.MapHttpAttributeRoutes(new MyRouteBuilder());
The other extensibility point is IInlineConstraintResolver. This interface is responsible for taking an inline constraint and manufacturing an IHttpRouteConstraint for that parameter:
publicinterface IInlineConstraintResolver { IHttpRouteConstraint ResolveConstraint(string inlineConstraint); }
config.MapHttpAttributeRoutes(new HttpRouteBuilder(new MyConstraintResolver()));
The default constraint resolver uses a dictionary based approach of mapping constraint keys to a particular constraint type. It contains logic to create an instance of the type based on the constraint arguments. It exposes the dictionary publicly so that you can add custom constraints without having to implement IInlineConstraintResolver. Here's an example:
DefaultInlineConstraintResolver constraintResolver = new DefaultInlineConstraintResolver(); constraintResolver.ConstraintMap.Add("phonenumber", typeof(PhoneNumberRouteConstraint)); config.MapHttpAttributeRoutes(new HttpRouteBuilder(constraintResolver));