This is an out of date design spec for Web API 2.0 Beta

For official documentation please follow: http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2






Attribute routing in Web API (2.0 Beta)

One of the limitations of Web API'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 gets called right on the action itself. For most cases, this should improve usability and make Web APIs simpler to build and maintain.

Scenarios

Scenario 1: Defining verb-based and action-based actions in the same controller

public class OrdersController : ApiController
{
    [HttpGet("orders/{id}")]
    public Order Get(int id) { }
    [HttpPost("orders/{id}/approve")]
    public void Approve(int id) { }
}

Scenario 2: Versioning controllers - different controllers for different versions of an API

[RoutePrefix("api/v1/customers")]
public class CustomersV1Controller : ApiController { ... }
[RoutePrefix("api/v2/customers")]
public class CustomersV2Controller : ApiController { ... }

Scenario 3: Nested controllers - hierarchical routing where one controller can be accessed as a sub-resource of another controller

public class MoviesController : ApiController
{
    [HttpGet("actors/{actorId}/movies")]
    public Movie Get(int actorId) { }
    [HttpGet("directors/{directorId}/movies")]
    public Movie Get(int directorId) { }
}

Scenario 4: Defining multiple ways to access a resource

public class PeopleController : ApiController
{
    [HttpGet("people/{id:int}")]
    public Person Get(int id) { }
    [HttpGet("people/{name}")]
    public Person Get(string name) { }
}
In the controller above, the request api/People/3 would match the first action because routes with constrained parameters are evaluated before unconstrained parameters.

Scenario 5: Defining actions with different parameter names

public class MyController : ApiController
{
    [HttpPost("my/action1/{param1}/{param2")]
    public void Action1(string param1, string param2) { }
    [HttpPost("my/action2/{x}/{y}")]
    public void Action2(string x, string y) { }
}

Scenario 6: Defining multiple ways to access a particular action

[RoutePrefix("orders")]
[RoutePrefix("customers/{customerId}/orders")]
public class OrdersController : ApiController
{
    [HttpGet("{orderId}")]
    [HttpGet("get/{orderId}")]
    public void Get(string customerId = null, string orderId) { }
}

Design

The experience for getting started with attribute-based routing will look something like this:
  1. Annotate the action with one of our HTTP verb attributes (HttpGet/HttpPost/HttpPut/HttpDelete/HttpPatch/HttpHead/HttpOptions/AcceptVerbs), passing in the route template in the constructor.
  2. Call the HttpConfiguration.MapHttpAttributeRoutes() extension method when configuring routes.
This call will use the controller selector and the action selector from the configuration to explore all the controllers and actions available and retrieve route-defining attributes. It will 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 MapHttpAttributeRoutes and still define regular routes using MapHttpRoute. Here's an example:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute("Default", "api/{controller}");
In most cases, MapHttpAttributeRoutes will be called first so that attribute routes are registered before the global routes (and therefore get a chance to supersede global routes).

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") { }
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 Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

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) { }
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 Person Get(int id) { }
Inline constraints must appear before the optional parameter '?' and default values, and the constraint resolution is extensible. See below for details.

Route prefixes

Frequently you’ll want to specify a common prefix for an entire controller. For example:

public class CustomersController : ApiController
{
    [HttpGet("customers")]
    public IEnumerable<Customer> Get() { }
    [HttpGet("customers/{id}")]
    public Customer Get(int id) { }
    [HttpPost("customers")]
    public void 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")]
public class CustomersController : ApiController
{
    public IEnumerable<Customer> Get() { }
    [HttpGet("{id}")]
    public Customer Get(int id) { }
    public void 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:
  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

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:
public class HttpRouteBuilder
{
    public virtual IHttpRoute BuildHttpRoute(string routeTemplate, IEnumerable<HttpMethod> httpMethods, string controllerName, string actionName);
    public virtual IHttpRoute BuildHttpRoute(HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, string routeTemplate);
}
You can pass in a custom HttpRouteBuilder to the MapHttpAttributeRoutes method:
config.MapHttpAttributeRoutes(new MyRouteBuilder());
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:
public interface IInlineConstraintResolver
{
    IHttpRouteConstraint ResolveConstraint(string inlineConstraint);
}
The inline constraint resolver is an argument to HttpRouteBuilder's constructor, so you could call the following:
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));

Last edited Apr 29 at 10:09 PM by yishaigalatzer, version 53

Comments

DotNetWise Nov 12, 2013 at 1:50 PM 
Scenario 3 is just stupid uncompilable code:
Two methods with same argument types (int) and same name (get).



public class MoviesController : ApiController
{
[HttpGet("actors/{actorId}/movies")]
public Movie Get(int actorId) { }
[HttpGet("directors/{directorId}/movies")]
public Movie Get(int directorId) { }
}

JaYmZ Nov 7, 2013 at 12:01 PM 
Updating [HttpGet] to [Route] attributes in here could be great, as it actuallly changed with the RTM version :)

benjamin1009 Aug 16, 2013 at 7:24 AM 
now i see the RoutePrefix support constraints ,that is very useful.
is there a plan we support IgnoreRoutePrefix and customer handler to the MapHttpAttributeRoutes ?

kichalla Jul 31, 2013 at 3:20 PM 
@benjamin1009:

Based on the error, looks like you also have an action which has a route like 'api/values/{id}' and looks like Get(int id)...if that's the case, the error is expected as the request would be matching this action and during parameter binding stage the failure is happening as its trying to binding 'test1' to 'id' parameter.

mikakolari Jul 2, 2013 at 6:13 PM 
There should be some kind of global/assembly prefix i.e.

config.MapHttpAttributeRoutes("api")

with attribute
[RoutePrefix("customers")]

would make a route like "api/customers"

SiggiG Jun 27, 2013 at 11:21 AM 
Any way to allow routes be built for inspecting other things than the URL and HTTP method? I'm thinking about versioning built on Accept-Type HTTP headers.

jgeurts May 30, 2013 at 7:28 PM 
Please allow for the case of creating our own routing attributes like [GetOrPost("people/{id}")].
+1 on removing Http prefix from the attribute names

timmerz May 30, 2013 at 4:46 PM 
I disagree with aliostad. What you guys have seems like the nodejs/express way. I like it. keep up the good work. that said, I agree with NaturalCause about removing Http prefix.

timmerz May 30, 2013 at 4:43 PM 
codeplex needs to use disquss

aliostad Apr 29, 2013 at 7:44 AM 
"Give a rope to people, they will hang themselves up with it" - anonymous

I have meant to write this comment weeks before but been busy and I hope it is not too late. Just fresh from finding a big performance bottleneck which turned out to be due to AttributeRouting, I think it is time to say a few words.

Routing is an area which needs improvement but the direction taken by adopting AttributeRouting is the opposite of what is needed. So instead of decentralising routes and hide the valuable information in string tokens, we need to move towards not only centralising the routing (as it is a strategic application-wide decision) but to allow the routes to be defined with zero configuration by the resources. And also the relationships to be defined naturally by the resources. So I believe it is definitely more like Darrel Miller's ApiRouter rather than AttributeRouting.

Another aspect is the hierarchical routing which also defines the relationships naturally with code rather than arbitrary string tokens. This allows hypermedia to be derived from the resource itself rather than to hand-coded in the resource. Reality is Web APIs are hierarchical while routing adopted from MVC is very flat. One of the problems is the linear search through the routes definitely does not scale.

I can do a proof of concept to show what I mean - if you think it is useful.

danroth27 Apr 26, 2013 at 5:32 PM 
The good news is that Tim McCall of AttributeRouting fame is the main contributor to pulling attribute into ASP.NET Web API, which means the design benefits from his extensive expertise. There is no intent to reinvent the API. Instead the goal is to integrate the API right into the runtime so that attribute routing becomes a first class experience (ex. we are integrating directly with the existing HttpGet/Post/Put/Delete attributes instead of creating new ones). That said, your feedback is critical to ensuring we do our job right, so by all means please grab a daily build and let us know what you think!

corinblaikie Apr 24, 2013 at 11:16 AM 
It would be nice if the routes could also be defined on interfaces too.

The use case for this is having an interface that defines the API and client classes / server classes that implement the interface.

youssefm Apr 3, 2013 at 7:34 PM 
@NaturalCause - don't worry, tim's attribute routing won't be changed in any way. It should still work just fine. You can keep using that if you like it better.

@Damian - You can do something like this:

[HttpGet("countries/{name?}")]
public Country GetCountry(string name = "USA") { }

and that should work fine. That way it's typed as a C# primitive. Is that similar enough to what you had in mind?

damianedwards Apr 3, 2013 at 3:32 AM 
Any way the default values could be typed instead of specified in the route string? Obviously only works for primitives as has to be defined in the attribute, e.g.

[HttpGet("countries/{name={0}}", "USA")]
public Country GetCountry(string name) { }

[HttpGet("people/{id:int={0}}", 1)]
public Person Get(int id) { }

If ordinal tokens aren't appealing, perhaps use the parameter name itself or some other token (value?):

[HttpGet("people/{id:int={id}}", 1)]
public Person Get(int id) { }

[HttpGet("people/{id:int={value}}", 1)]
public Person Get(int id) { }

NaturalCause Apr 3, 2013 at 2:41 AM 
I seriously SERIOUSLY hope you don't mess up the existing implementation of AttributeRouting. All the hard work has already been done, there's no need to reinvent the API.

There's NO reason to prefix all the verbs with Http, we know GET is an Http verb. Just make it GET/POST/PUT etc, and do away with Http on the front.