A single project can use convention and attribute routing at the same time.
Actions are still selected top-down, i.e. the first matching action in the controller will be selected.
aka Convention based routing
Convention routing defines all route patterns in one place.
To enable convention routing:
using System.Web.Http;
namespace WebApplication
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
aka Attribute based routing
Attribute routing is better than convention routing at supporting RESTful URI patterns.
A RESTful API commonly contains routes like "/customers/<id>/orders/<id>/items".
Convention routing is built to support routes like "/customers", "/orders", and "/items".
You can add a Route Attribute to a Controller and/or an Action.
The Action's route will be appended to the end of the Controller's route.
Controllers and Actions without a route attribute will default to convention routing.
You can apply multiple routes to one Controller and/or Action. All specified routes will lead to that Controller/Action.
If the Controller and the Action have multiple routes, all possible combinations will be evaluated.
Format of URI templates:
//literal path segment
[Route("abc")]
//path seperator
[Route("abc/123")]
//reference parameter value
[Route("abc/{customerId}/orders")]
public ActionResult GetCustomerOrders(long customerId) { }
//overloading URI segments
[Route("abc/{id}")]
[Route("abc/xyz")]
To define a common route prefix for all Actions in a Controller:
[RoutePrefix("api/v1")]
public class MyController : ApiController { }
Route prefixes can include parameters that will be matched based on the Action.
?How is this different from defining a route on the controller?
To override a route prefix on an Action, use a tilde-slash:
[Route("~/api/v1/customers")]
public ActionResult MyAction() { }
Or just a slash:
[Route("/api/v1/customers")]
public ActionResult MyAction() { }
To make a URI parameter optional, suffix with question mark and provide a default value:
[Route("books/{lcid:int?}")]
public ActionResult GetBooks(int lcid = 1033) { }
//or this
[Route("books/{lcid:int=1033}")]
public ActionResult GetBooks(int lcid) { }
//this requires "1033" to go through normal model binding
To specify a wildcard or catch-all segment, prefix with an asterisk:
[Route("customers/{*queryValues}")]
public ActionResult MyAction(string queryValues) { }
Wildcards can accept single or multiple segments.
Wildcards can still use constraints.
To enable attribute routing:
using System.Web.Http;
namespace WebApplication
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
}
Token replacement in route templates:
[Route("api/v1/[controller]/[action]", Name="[controller]_[action]")]
"[area]" is replaced with the Area name.
"[controller]" is replaced with the Controller name.
"[action]" is replaced with the Action name.
Control characters "[" and "]" can be escaped as "[[" and "]]".
Token replacement can be customized with a parameter transformer. For example, route value "SubscriptionManagement" can be transformed into "subscription-management".
//custom transformer
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null)
return null;
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
}
//register convention
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});
}
//route evaluation
public class SubscriptionManagementController : Controller
{
[HttpGet("[controller]/[action]")] //matches /subscription-management/list-all
public IActionResult ListAll() { }
}
Complex segments are processed by matching up literals from right-to-left in a non-greedy way.
[Route("/abc{x}xyz")]
Attribute routing can use route constraints.
Route constraints limit what route parameters can be matched to.
Format {parameter:constraint}
[Route("customers/{x:int}")] //integers only
[Route("customers/{x:long}")] //longs only
[Route("customers/{x:decimal}")] //decimals only
[Route("customers/{x:double}")] //doubles only
[Route("customers/{x:float}")] //floats only
[Route("customers/{x:alpha}")] //a-zA-Z only
[Route("customers/{x:bool}")] //boolean only
[Route("customers/{x:datetime}")] //datetime only
[Route("customers/{x:guid}")] //GUID only
[Route("customers/{x:min(100)}")] //only integers >= 100
[Route("customers/{x:max(100)}")] //only integers <= 100
[Route("customers/{x:range(10, 100)}")] //only integers from 10 to 100
[Route("customers/{x:length(6)}")] //only strings of length 6
[Route("customers/{x:length(10,100)}")] //only strings of length 10 through 100
[Route("customers/{x:minlength(6)}")] //only strings of length 6 or more
[Route("customers/{x:maxlength(6)}")] //only strings of length up to 6
[Route("customers/{x:regex(^\d\d-\d\d-\d\d\d\d$)}")] //only strings matching a pattern
Multiple constraints use a colon delimiter:
[Route(customers/{x:int:min(100)}")]
Custom route constraints can be made by inheriting from IHttpRouteConstraint.
//define constraint
public class MyConstraint : IHttpRouteConstraint
{
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
//your code here
}
}
//register constraint
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("mine", typeof(MyConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
}
}
//use constraint
[Route("customers/{id:mine}")]
In Web API, every route has a name. This is useful for returning links.
To specify a name:
[Route("api/books/{id}", Name="GetBookById")]
public ActionResult GetBook(int id) { }
//elsewhere
string uri = Url.Link("GetBookById", new { id = book.Id });
Routes are always evaluated in a particular order.
You can specify a custom order.
Evaluation order depends on:
- Order property first
- Then, for each URI segment
- Literal segments first
- Then parameter segements with constraints
- Then parameter segements without constraints
- Then wildcard segments with constraints
- Then wildcard segments without constraints
- Finally, by the literal string-sort order of the route templates
[Route("pending", RouteOrder=1)]
The default value is 0.
Routing attributes will be inherited:
[Route("api/v1/[controller]")]
public abstract class MyBaseController : Controller { }
public class ProductsController : MyBaseController
{
[HttpGet] //matches /api/v1/Products
public IActionResult List() { }
[HttpPut("{id}")] //matches /api/v1/Products/{id}
public IActionResult Edit(int id) { }
}
An action named "PutCustomers" will default to matching HTTP verb "PUT" even without the "[HttpPut]" attribute. If an HTTP verb attribute is specified, it will override the action name.
This works for verbs DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.
For other verbs, use the AcceptVerbs attributes, such as [AcceptVerbs("MKCOL")].
You can define routes directly in an HTTP verb attribute:
[HttpGet("{id}")]
public ActionResult Customers(int id) { }
By default, Web API gets the values of primitive parameters from the query string, and complex parameters from the request body.
Specify value is bound from the query string:
public IActionResult MyAction([FromUri] ComplexType input) { }
Specify value is bound from request body:
public IActionResult MyAction([FromBody] string input) { }
You can specify which model binder to use on each object:
[ModelBinder(typeof(MyModelBinder))]
public class MyClass { }
Or on each Action parameter:
public IActionResult MyAction([ModelBinder(typeof(MyModelBinder))] MyEnum input) { }
Or register it for the entire application:
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class MyModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(TypeToApplyModelBinderTo))
return new MyModelBinder();
return null;
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new MyModelBinderProvider())
);
}
By defining string values on your enum, the default model binder will convert incoming strings to enums.
public enum MyEnum
{
[EnumMember(Value = "AAA")] Aaa,
[EnumMember(Value = "BBB")] Bbb,
[EnumMember(Value = "CCC")] Ccc
}
public class MyType
{
public string Name { get; set; }
public MyEnum Code { get; set; }
}
public IActionResult MyAction(MyType input) { }
//JSON request body { Name:"Bob", Code:"CCC" }
//input { Name="Bob", Code=MyEnum.Ccc }
Model bind string value of an enum to the enum value.
Enum
public enum MyEnum
{
[EnumMember(Value = "AAA")] Aaa,
[EnumMember(Value = "BBB")] Bbb,
[EnumMember(Value = "CCC")] Ccc
}
Model Binder
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
internal class StringEnumModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
if (!bindingContext.ModelType.IsEnum)
throw new InvalidOperationException("ModelType is not an Enum.");
Type enumType = bindingContext.ModelType;
ValueProviderResult values = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
if (values.Length == 0)
return Task.CompletedTask;
string enumString = values.FirstValue;
Object enumValue = Activator.CreateInstance(enumType);
if (Enum.TryParse(enumType, enumString, true, out enumValue))
{
bindingContext.Result = ModelBindingResult.Success(enumValue);
}
return Task.CompletedTask;
}
}