Attribute Routing in Web API 2
This tutorial uses pre-release features.
Routing is how Web API matches a URI to an action. Web API 2 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 API. For example, you can easily create URIs that describe hierarchies of resources.
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 topic shows how to enable attribute routing and describes the various options for attribute routing. For an end-to-end tutorial that uses attribute routing, see Create a REST API with Attribute Routing in Web API 2.
- Why Attribute Routing?
- Enabling Attribute Routing
- Adding Route Attributes
- Route Constraints
- Route Prefixes
- Optional URI Parameters and Default Values
- Route Names
- Route Order
- Extensibility
Prerequisites
To use attribute routing, install Visual Studio 2013 Preview or Visual Studio Express 2013 Preview for Web.
Alternatively, use NuGet Package Manager to install the necessary packages. From the Tools menu in Visual Studio, select Library Package Manager, then select Package Manager Console. Enter the following command in the Package Manager Console window:
Install-Package Microsoft.AspNet.WebApi.WebHost -Pre
Why Attribute Routing?
The first release of Web API used convention-based routing. In that type of routing, you define one or more route templates, which are basically parameterized strings. When the framework receives a request, it matches the URI against the route template. (For more information about convention-based routing, see Routing in ASP.NET Web API.
One advantage of convention-based routing is that templates are defined in a single place, and the routing rules are applied consistently across all controllers. Unfortunately, convention-based routing makes it hard to support certain URI patterns that are common in RESTful APIs. For example, resources often contain child resources: Customers have orders, movies have actors, books have authors, and so forth. It’s natural to create URIs that reflect these relations:
/customers/1/orders
This type of URI is difficult to create using convention-based routing. Although it can be done, the results don’t scale well if you have many controllers or resource types.
With attribute routing, it’s trivial to define a route for this URI. You simply add an attribute to the controller action:
[HttpGet("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
Here are some other patterns that attribute routing makes easy.
API versioning
In this example, “/api/v1/products” would be routed to a different controller than “/api/v2/products”.
/api/v1/products
/api/v2/products
Overloaded URI segments
In this example, “1” is an order number, but “pending” maps to a collection.
/orders/1
/orders/pending
Mulitple parameter types
In this example, “1” is an order number, but “2013/06/16” specifies a date.
/orders/1
/orders/2013/06/16
Enabling Attribute Routing
To enable attribute routing, call MapHttpAttributeRoutes during configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
This extension method is defined in the System.Web.Http.HttpConfigurationExtensions class.
You can also combine attribute routing with convention-based routing. To define convention-based routes, call the MapHttpRoute method.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Adding Route Attributes
Here is an example of a route defined using an attribute:
public class OrdersController : ApiController
{
[HttpGet("customers/{customerId}/orders")]
public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}
The [HttpGet] attribute defines an HTTP GET method. The string "customers/{customerId}/orders" is the URI template for the route. Notice that the "{customerId}" parameter in the route template matches the name of the customerId parameter in the method. For example, this route would match the following URI:
http://example.com/customers/1/orders
The URI template can have more than one parameter:
[HttpGet("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
The following route attributes are defined:
HTTP Method | Attribute |
---|---|
DELETE | [HttpDelete] |
GET | [HttpGet] |
HEAD | [HttpHead] |
OPTIONS | [HttpOptions] |
PATCH | [HttpPatch] |
POST | [HttpPost] |
PUT | [HttpPut] |
Other | [AcceptVerbs] |
Use the [AcceptVerbs] attribute to specify multiple HTTP methods for an action, or to specify HTTP methods that are not listed in the table.
[AcceptVerbs("GET", "PUT", "PROPFIND", RouteTemplate="/customers/{id}")]
Any controller methods that do not have a route attribute use convention-based routing. That way, you can combine both types of routing in the same project.
Route Constraints
Route constraints let you restrict how the parameters in the route template are matched. The general syntax is "{parameter:constraint}". For example:
[HttpGet("users/{id:int}"]
public User GetUserById(int id) { ... }
[HttpGet("users/{name}"]
public User 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:(^\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.
[HttpGet("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }
Route Prefixes
Often, the routes in a controller all start with the same prefix. For example:
public class BooksController : ApiController
{
[HttpGet("api/books")]
public IEnumerable<Book> GetBooks() { ... }
[HttpGet("api/books/{id:int}")]
public Book GetBook(int id) { ... }
[HttPost("api/books")]
public HttpResponseMessage CreateBook(Book book) { ... }
}
You can set a common prefix for an entire controller by using the [RoutePrefix] attribute:
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
public IEnumerable<Book> Get() { ... }
[HttpGet("{id:int}")]
public Book Get(int id) { ... }
public HttpResponseMessage Post(Book book) { ... }
}
You can put more than one [RoutePrefix] attribute on a controller. For example:
[RoutePrefix("api/books")]
[RoutePrefix("api/v1/books")]
public class BooksController : ApiController
{
public IEnumerable<Book> Get() { ... }
[HttpGet("{id:int}")]
public Book Get(int id) { ... }
public HttpResponseMessage Post(Book book) { ... }
}
In this example, /api/books/1
and /api/v1/books/1
return the same resource.
The route prefix can include parameters in the route template:
[RoutePrefix("customers/{customerId}")]
public class MyController : ApiController
{
// GET customers/1/orders
[HttpGet("orders")]
public IEnumerable<Order> Get(int customerId) { ... }
}
Optional URI Parameters and Default Values
You can make a URI parameter optional by adding a question mark to the route parameter. If a route parameter is optional, you must define a default value for the method parameter.
public class BooksController : ApiController
{
[HttpGet("api/books/locale/{lcid?}")]
public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}
In this example, /api/books/locale/1033
and /api/books/locale
return the same resource.
Alternatively, you can specify a default value inside the route template, as follows:
public class BooksController : ApiController
{
[HttpGet("api/books/locale/{lcid=1033}")]
public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}
This is almost the same as the previous example, but there is a slight difference of behavior when the default value is applied.
- In the first example ("{lcid?}"), the default value of 1033 is assigned directly to the method parameter, so the parameter will have this exact value.
- In the second example ("{lcid=1033}"), the default value of "1033" goes through the model-binding process. The default model-binder will convert "1033" to the numeric value 1033. However, you could plug in a custom model binder, which might do something different.
(In most cases, unless you have custom model binders in your pipeline, the two forms will be equivalent.)
Route Names
In Web API, every route has a name. Route names are useful for generating links, so that you can include a link in an HTTP response.
To specify the route name, set the RouteName property on the attribute. The following example shows how to set the route name, and also how to use the route name when generating a link.
public class BooksController : ApiController
{
[HttpGet("api/books/{id}", RouteName="GetBookById")]
public BookDto GetBook(int id)
{
// Implementation not shown...
}
[HttpPost("api/books")]
public HttpResponseMessage Post(Book book)
{
// Validate and add book to database (not shown)
var response = Request.CreateResponse(HttpStatusCode.Created);
// Generate a link to the new book and set the Location header in the response.
string uri = Url.Link("GetBookById", new { id = book.BookId });
response.Headers.Location = new Uri(uri);
return response;
}
}
If you don't set the RouteName property, Web API generates the name. The default route name is "ControllerName.ActionName". In the previous example, the default route name would be "Books.GetBook" for the GetBook
method. If the controller has multiple attribute routes with the same action name, a suffix is added; for example, "Books.GetBook1" and "Books.GetBook2".
Route Order
When the framework tries to match a URI with a route, it evaluates the routes in a particular order. To specify the order, set the RouteOrder property on the route attribute, or set the Order property on the [RoutePrefix] attribute. Lower values are evaluated first. The default order value is zero.
Here is how the total ordering is determined:
- Compare the Order property of the route prefix. (If the controller does not have a [RoutePrefix] attribute, treat the route prefix order as zero.)
- Compare the RouteOrder property of the route attribute.
- Look at each URI segment in the route template. For each segment, order as follows:
- Literal segments.
- Route parameters with constraints.
- Route parameters without constraints.
- Wildcard parameter segments with constraints.
- Wildcard parameter segments without constraints.
- In the case of a tie, routes are ordered by a case-insensitive ordinal string comparison (OrdinalIgnoreCase) of the route template.
Here is an example. Suppose you define the following controller:
[RoutePrefix("orders")]
public class OrdersController : ApiController
{
[HttpGet("{id:int}")] // constrained parameter
public HttpResponseMessage Get(int id) { ... }
[HttpGet("details")] // literal
public HttpResponseMessage GetDetails() { ... }
[HttpGet("pending", RouteOrder = 1)]
public HttpResponseMessage GetPending() { ... }
[HttpGet("{customerName}")] // unconstrained parameter
public HttpResponseMessage Get(string customerName) { ... }
[HttpGet("{*date:datetime}")] // wildcard
public HttpResponseMessage Get(DateTime date) { ... }
}
These routes are ordered as follows.
- orders/details
- orders/{id}
- orders/{customerName}
- orders/{*date}
- orders/pending
Notice that "details" is a literal segment and appears before "{id}", but "pending" appears last because the RouteOrder property is 1.
Extensibility
To control how routes are created, extend System.Web.Http.Routing.HttpRouteBuilder. The HttpRouteBuilder class takes a tokenized route template and creates an IHttpRoute. To use a custom implementation, derive from HttpRouteBuilder and pass an instance of your derived class to the MapHttpAttributeRoutes method.
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes(new MyRouteBuilder());
}
To control how route constraints are parsed, implement the IInlineConstraintResolver interface. Pass your implementation to the HttpRouteBuilder constructor.
public class MyConstraintResolver : IInlineConstraintResolver
{
public IHttpRouteConstraint ResolveConstraint(string inlineConstraint)
{
// ...
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes(new HttpRouteBuilder(new MyConstraintResolver()));
}
}
Alternatively, the default IInlineConstraintResolver stores a dictionary of constraints, so you can add your own constraints without implementing IInlineConstraintResolver.
public class PhoneNumberConstraint : IHttpRouteConstraint
{
// ...
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("phone", typeof(PhoneNumberConstraint));
config.MapHttpAttributeRoutes(new HttpRouteBuilder(constraintResolver));
}
}
Comments (0) RSS Feed