Attribute routing in Web API
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.
Scenarios
Scenario 1: Defining verb-based and action-based actions in the same controller
publicclass OrdersController : ApiController { [HttpGet("orders/{id}")] public Order Get(int id) { } [HttpPost("orders/{id}/approve")] publicvoid Approve(int id) { } }
Scenario 2: Versioning controllers - different controllers for different versions of an API
[RoutePrefix("api/v1/customers")] publicclass CustomersV1Controller : ApiController { ... } [RoutePrefix("api/v2/customers")] publicclass CustomersV2Controller : ApiController { ... }
Scenario 3: Nested controllers - hierarchical routing where one controller can be accessed as a sub-resource of another controller
publicclass 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
publicclass PeopleController : ApiController { [HttpGet("people/{id:int}")] public Person Get(int id) { } [HttpGet("people/{name}")] public Person Get(string name) { } }
Scenario 5: Defining actions with different parameter names
publicclass MyController : ApiController { [HttpPost("my/action1/{param1}/{param2")] publicvoid Action1(string param1, string param2) { } [HttpPost("my/action2/{x}/{y}")] publicvoid Action2(string x, string y) { } }
Design
The experience for getting started with attribute-based routing will look something like this:- 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.
- Call the HttpConfiguration.MapHttpAttributeRoutes() extension method when configuring routes.
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}");
Optional parameters and default values
You will be able to specify that a parameter is optional by adding a question mark to the parameter, e.g.,[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 a particular minimum length | {x:minlength(4)} |
maxlength | Matches a string with a particular maximum length | {x:maxlength(8)} |
length | Matches a string with a particular length or within a particular range of lengths | {x:length(6)}, {x:length(4,8)} |
min | Matches an integer with a particular minimum value | {x:min(100)} |
max | Matches an integer with a particular maximum value | {x:max(200)} |
range | Matches an integer within a particular range of values | {x:range(100,200)} |
alpha | Matches English uppercase or lowercase alphabet characters | {x:alpha} |
regex | Matches a particular 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
A common case is to want to specify a common prefix for an entire controller. Think of this 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 could turn into this:
[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));