Attribute routing is a great way to keep the routing for your RESTful API’s under control. Introduced with Web API 2, Attribute Routing makes a few things simpler compared to the routing templates used in the first revision of ASP.NET Web API.

Simplification by Atribute Routing

A few of the things that got better or simpler with Attribute Routing.

Multiple Parameter Types

Attribute routing easily enables using multiple parameter types in a route. An example route taking multiple parameters:

[HttpGet("api/entity/{id:int}/{foo:length(5)}")]

In the example the id parameter is required to be an int and the foo parameter is required to be a string of 5 characters.

Versioning of an API

Versioning of APIs has always been a little tricky (and debated), but one thing attribute routing enables is to easily create versioned routes of the format:

[HttpGet("api/v1/entity/{id:int}")]

URI Segment Overloads

Having multiple HTTP GET endpoints in a controller for different URIs is easy, for ex two endpoints taking either an int or a string as the last parameter:

[HttpGet("api/entity/{id:int}")]
[HttpGet("api/entity/{foo:length(5)}")]

The Controller and Action Tokens

A problem with the ASP.NET Web API previously have been renaming of controllers or actions and any eventual string references to their name in the routes. In the ASP.NET Core Web API we’ve got a remedy for that, enter [controller] and [action] token!

Just use these tokens in any routes when you want to reference the controller or action name.

Setting a Controller Base Route

A common practice in ASP.NET Core Web API controllers is to define a base route that all the endpoints in the class will have in common. You do this by defining a route attribute just above the class declaration for the controller. Like this:

[Route("api/[controller]")]
public class ProductsController : Controller
{
...
}

This will make all endpoints in the controller have “/api/products/” prefixed in front of them, thus you won’t have to specify that in every methods attribute route.

Overriding a Controllers Base Route

If you want to define an endpoint without the base route in a controller that has a base route, it’s possible to use the tilde character (“~”) to override the rule.
For ex, putting the attribute route below on the Get method in the DroidsController used for the examples in this post:

[HttpGet("~/thesearethedroids")]

This template URI would make the route change from http://localhost:5000/api/droids/ to http://localhost:5000/thesearethedroids/

Anatomy of an Attribute Route

An attribute route is defined as follows:

[HttpMethod("Template URI", Name?, Order?)]

First, let’s take a look at the available Http methods in the ASP.NET Core Web API.

Http Methods

These are the currently available Http Methods in ASP.NET Core 1.0, click the link to see the documentation for each method.

HttpDeleteAttribute

Used to define a HTTP DELETE method.

HttpGetAttribute

Used to define a HTTP GET method.

HttpHeadAttribute

Used to define a HTTP HEAD method.

HttpOptionsAttribute

Used to define a HTTP OPTIONS method.

HttpPatchAttribute

Used to define a HTTP PATCH method.

HttpPostAttribute

Used to define a HTTP POST method.

HttpPutAttribute

Used to define a HTTP PUT method

The Template URI Parameter

A Route is a string describing a URI template. The template consists of literals and/or parameters, parameters are defined within curly brackets. The following is an example of a route template. In the example “droids” and “services” are literals and “{id}” is a parameter.
/droids/{id}/services/

An issue with the above template is that the “{id}” parameter is unspecified, meaning that the following routes will all match this URI template:
/droids/10/services/
/droids/true/services/
/droids/theseare/services/
/droids/1.97/services/
/droids/mobilemancer/services/
/droids/r2-d2/services/

The way to go for making the routes more specific is Routing Constraints.
A Routing Constraint is used to set rules on your parameters. For example, setting the constraint that the above example routes “{id}” parameter should be an int would be done as follows:
/droids/{id:int}/services/
More on Routing Constraints in a section below.

The Name Parameter

An optional Name parameter can be given in an attribute route, this can then be used when creating links to the route.
For example, when creating a resource with HTTP PUT, the response is often a CreatedAtRouteResult.
The CreatedAtRouteResult has two constructors, one taking a routeName string as first parameter, the other depends on the parameter routeValues to create a valid URI. Examples below are referring to the GetDroidById method shown in the routing constraint example below, the one defined to show using int as a constraint,
Creating a CreatedAtRouteResult using the Name parameter defined on a route:

new CreatedAtRouteResult("GetDroidById", new { id = droid.Id }, droid);

Creating a CreatedAtRouteResult using the values in the routeValues to construct the URI:

new CreatedAtRouteResult(new { Controller = "droids", Action = nameof(GetById), id = droid.Id }, droid)

Personally I’d avoid using the Name parameter as it only leads to usage of “magic strings”, it’s just to easy to miss these when refactoring methods in the API.

The Order Parameter

When using HTTP Method overloads, it’s important to keep track of in which order the endpoints will be executed. There is a definite ordering of routes that are dependent on literals, parameters, constraints and also the Order parameter. The ordering happens as follows:
1 – A comparison of the Order parameter.
2 – Literal segments.
3 – Route parameters with constraints.
4 – Route parameters without constraints.
5 – Wildcard parameter segments with constraints.
6 – Wildcard parameter segments without constraints.

Note! Do note that the usage of the Order parameter is based on comparison between different attribute signatures – meaning that if you only define the Order parameter on one route there will be no comparison against the other routes and the rest of the rules will define the inter-mutual ordering.

Routing Constraints

In the ASP.NET Core 1.0 Web API implementation, there is currently 17 different route constraints available. They could be more or less be divided in a few groups, being Type Based Constraints, String Length Constraints, Value Constraints and Regex-based Constraints.

We’ll take a look at all the constraints available in the ASP.NET Core Web API later, but first let’s look at how to use the route constraints.

Using Route Constraints

Using the constraints is done by adding a colon after parameters and then adding the specific constraint. An example of how to make the droid id only accept guid‘s as id’s:
/droids/{id:guid}/services/

Constraints taking values are specified by entering the value in a parentheses after the constraint. Here’s an example specifying that the droid id is a string that is not shorter than three chars:
/droids/{id:minlength(3)}/services/

It’s also possible to specify multiple restraints on a parameter, this is done by just adding another colon and another constraint. This example specifies that a droid’s id is an int and has a minimum value of five:
/droids/{id:int:min(3)}/services/

Type Based Constraints

There is eight different constraints used for specifying what type is expected, let’s take a look at them.
But first let’s call the droids controller without any parameters, it will list all the droids in the repository. This way you know what the data looks like and know what to expect when we start running the constraint examples below.

Calling the Web API can be done with several different tools.
My preferred method is to use Postman. I use Postman because it’s a great tool and because it enables me to save my requests, so I can work from multiple computers.

Call ASP.NET Core Web API from Postman
Using Postman to call the Web API

Another quick and easy test tool is to use the Invoke-RestMethod command from PowerShell.
Calling the GetAll base route from PowerShell would look like this:
Invoke-RestMethod http://localhost:5000/api/droids/
The result looks like this:

Using PowerShell to call an ASP.NET Core Web API
Calling our ASP.NET Core Web API from PowerShell

To transform the result-set in PowerShell to JSon, pipe the call through ConvertTo-Json, like this:
Invoke-RestMethod http://localhost:5000/api/droids/ | ConvertTo-Json
Note! A small tip, if you need to copy and paste the PowerShell result somewhere – Just append a “| clip” to the end and the result will be in your clipboard!

This is what the response looks like when piped through ConvertTo-Json in PowerShell:

{
[
{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-05T14:09:39.3426236Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
},
{
"id":  1,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-05T14:09:39.3426236Z",
"name":  "C-3PO",
"creditBalance":  0,
"productSeries":  "3PO-series Protocol Droid",
"height":  1.71,
"armaments":  [

],
"equipment":  [
"TranLang III communication module"
]
},
{
"id":  2,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-05T14:09:39.3426236Z",
"name":  "R2-D2",
"creditBalance":  0,
"productSeries":  "R-Series",
"height":  0.96,
"armaments":  [
"Buzz saw",
"Electric pike"
],
"equipment":  [
"Drinks tray (only on sail barge)",
"Fusion welder",
"Com link",
"Power recharge coupler",
"Rocket boosters",
"Holographic projector/recorder",
"Motorized, all-terrain treads",
"Retractable third leg",
"Periscope",
"Fire extinguisher",
"Hidden lightsaber compartment with ejector",
"Data probe",
"Life-form scanner",
"Utility arm"
]
}
]
}

The above dataset is what we’re going to use for all examples below that use the DroidsController (to get the code, see the last section of this post).

Now, let’s get on with the Route Constraint examples. They are presented in a brief summary, then the code to implement it in the Web API and then finally the result when calling the endpoint in PowerShell.

Int Route Constraint

Below an example of using the int constraint, using it to specify a droid id.

/// <summary>
/// Int as constraint
/// </summary>
/// <param name="id">droid id</param>
/// <returns>A droid with a specific Id</returns>
[HttpGet("{id:int}", Name = "GetDroidById", Order = 0)]
public IActionResult GetById(int id)
{
var droid = droidRepo.Get(id);

if (droid == null)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"Droid with id: {id} - Not found in database!"
}
);
}

return new OkObjectResult(droid);
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/api/droids/0 | ConvertTo-Json
Gives the response:

{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-05T14:02:03.1161577Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
}

Bool Route Constraint

Here’s an example of using the bool constraint, toggling if the droids armaments should be included in the response.

/// <summary>
/// Bool as constraint
/// </summary>
/// <param name="withWeapons">toggle armaments</param>
/// <returns>A droid with or without armaments</returns>
[HttpGet("{withWeapons:bool}")]
public IActionResult GetWithArmaments(bool withWeapons)
{
var droids = droidRepo.GetAll();
if (droids == null)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droids found in database!"
}
);
}

if (!withWeapons)
{
foreach (var droid in droids)
{
droid.Armaments = Enumerable.Empty<string>();
}
}
return new OkObjectResult(droids);
}

Calling the route from Powershell like this:
Invoke-RestMethod http://localhost:5000/api/droids/false | ConvertTo-Json
Gives the response:

{
[
{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-05T15:09:10.9743745Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [

],
"equipment":  [

]
},
{
"id":  1,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-05T15:09:10.9743745Z",
"name":  "C-3PO",
"creditBalance":  0,
"productSeries":  "3PO-series Protocol Droid",
"height":  1.71,
"armaments":  [

],
"equipment":  [
"TranLang III communication module"
]
},
{
"id":  2,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-05T15:09:10.9743745Z",
"name":  "R2-D2",
"creditBalance":  0,
"productSeries":  "R-Series",
"height":  0.96,
"armaments":  [

],
"equipment":  [
"Drinks tray (only on sail barge)",
"Fusion welder",
"Com link",
"Power recharge coupler",
"Rocket boosters",
"Holographic projector/recorder",
"Motorized, all-terrain treads",
"Retractable third leg",
"Periscope",
"Fire extinguisher",
"Hidden lightsaber compartment with ejector",
"Data probe",
"Life-form scanner",
"Utility arm"
]
}
]
}

DateTime Route Constraint

This is an example of using the DateTime route constraint, here we are using it to specify how old an droid entry can be for it to be included in the response.

/// <summary>
/// DateTime as a constraint
/// </summary>
/// <param name="entryDate">date of registration in the galactic registry</param>
/// <returns>all droids registered in the galactic registry after a specified date</returns>
[HttpGet("{entryDate:datetime}")]
public IActionResult GetByEntryDate(DateTime entryDate)
{
var droids = droidRepo.GetAllFromEntryDate(entryDate);
if (droids == null || droids?.Count() == 0)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droids found in database created after {entryDate}!"
}
);
}
return new OkObjectResult(droids);
}

Calling the route from Powershell like this:
Invoke-RestMethod http://localhost:5000/api/droids/2016-07-04 | ConvertTo-Json
Gives the response:

{
[
{
"id":  1,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-05T14:02:03.1171574Z",
"name":  "C-3PO",
"creditBalance":  0,
"productSeries":  "3PO-series Protocol Droid",
"height":  1.71,
"armaments":  [

],
"equipment":  [
"TranLang III communication module"
]
},
{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-05T14:02:03.1161577Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
},
{
"id":  2,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-05T14:02:03.1171574Z",
"name":  "R2-D2",
"creditBalance":  0,
"productSeries":  "R-Series",
"height":  0.96,
"armaments":  [
"Buzz saw",
"Electric pike"
],
"equipment":  [
"Drinks tray (only on sail barge)",
"Fusion welder",
"Com link",
"Power recharge coupler",
"Rocket boosters",
"Holographic projector/recorder",
"Motorized, all-terrain treads",
"Retractable third leg",
"Periscope",
"Fire extinguisher",
"Hidden lightsaber compartment with ejector",
"Data probe",
"Life-form scanner",
"Utility arm"
]
}
]
}

Decimal Route Constraint

This is an example of the decimal constraint, used to specify a minimum height of droids.

/// <summary>
/// Decimal as a constraint
/// </summary>
/// <param name="height">a given height</param>
/// <returns>all droids over a given height</returns>
[HttpGet("{height:decimal}", Order = 7)]
public IActionResult GetByHeightDecimal(decimal height)
{
var droids = droidRepo.GetAllTallerThan(height);
if (droids == null || droids?.Count() == 0)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droids found in database taller than {height}!"
}
);
}
return new OkObjectResult(droids);
}

Calling the route from Powershell like this:
Invoke-RestMethod http://localhost:5000/api/droids/1.90 | ConvertTo-Json
Gives the response:

{
[
{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-06T00:13:57.0333804Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
}
]
}

Double Route Constraint

The following example shows an overload of getting droid heights specified as a double.

/// <summary>
/// Double as a constraint
/// </summary>
/// <param name="height">a given height</param>
/// <returns>all droids over a given height</returns>
[HttpGet("{height:double}", Order = 3)]
public IActionResult GetByHeightDouble(double height)
{
decimal convertedHeight = (decimal)height;
var droids = droidRepo.GetAllTallerThan(convertedHeight);
if (droids == null || droids?.Count() == 0)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droids found in database taller than {height}!"
}
);
}
return new OkObjectResult(droids);
}

Calling the route from Powershell like this:
Invoke-RestMethod http://localhost:5000/api/droids/100E-2 | ConvertTo-Json
Gives the response:

{
[
{
"id":  1,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-06T00:13:57.0343798Z",
"name":  "C-3PO",
"creditBalance":  0,
"productSeries":  "3PO-series Protocol Droid",
"height":  1.71,
"armaments":  [

],
"equipment":  [
"TranLang III communication module"
]
},
{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-06T00:13:57.0333804Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
}
]
}

Float Route Constraint

And another overload for getting droid height using a float. (Yeah, running out of good examples here, I know 🙁 )

/// <summary>
/// Float as a constraint
/// </summary>
/// <param name="height">a given height</param>
/// <returns>all droids over a given height</returns>
[HttpGet("{height:float}", Order = 4)]
public IActionResult GetByHeightFloat(float height)
{
decimal convertedHeight = (decimal)height;
var droids = droidRepo.GetAllTallerThan(convertedHeight);
if (droids == null || droids?.Count() == 0)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droids found in database taller than {height}!"
}
);
}
return new OkObjectResult(droids);
}

Calling the route from Powershell like this:
Invoke-RestMethod http://localhost:5000/api/droids/0.95 | ConvertTo-Json
Gives the response:

[
{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-06T00:26:27.6872656Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
},
{
"id":  1,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-06T00:26:27.6882653Z",
"name":  "C-3PO",
"creditBalance":  0,
"productSeries":  "3PO-series Protocol Droid",
"height":  1.71,
"armaments":  [

],
"equipment":  [
"TranLang III communication module"
]
},
{
"id":  2,
"imperialContractId":  "00000000-0000-0000-0000-000000000000",
"entryDate":  "2016-07-06T00:26:27.6882653Z",
"name":  "R2-D2",
"creditBalance":  0,
"productSeries":  "R-Series",
"height":  0.96,
"armaments":  [
"Buzz saw",
"Electric pike"
],
"equipment":  [
"Drinks tray (only on sail barge)",
"Fusion welder",
"Com link",
"Power recharge coupler",
"Rocket boosters",
"Holographic projector/recorder",
"Motorized, all-terrain treads",
"Retractable third leg",
"Periscope",
"Fire extinguisher",
"Hidden lightsaber compartment with ejector",
"Data probe",
"Life-form scanner",
"Utility arm"
]
}
]

Note! Make sure you get your Order parameters correct if you are implementing endpoints that take float, double and decimal in the same Controller!

Guid Route Constraint

Here’s a route for getting droids by their imperial contract id, and they are of course specified with guid’s.

/// <summary>
/// Guid as a constraint
/// </summary>
/// <param name="contractId">imperial contract id</param>
/// <returns>an eventual droid matching given contract id</returns>
[HttpGet("{contractId:guid}")]
public IActionResult GetByImperialContractId(Guid contractId)
{
var droid = droidRepo.GetByImperialId(contractId);
if (droid == null)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droid found in database with a imperial contract id of {contractId}!"
}
);
}
return new OkObjectResult(droid);
}

Calling the route from Powershell like this:
Invoke-RestMethod http://localhost:5000/api/droids/0B450FDD-F484-423B-8685-4193E9FA583D | ConvertTo-Json
Gives the response:

{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-06T00:26:27.6872656Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
}

Long Route Constraint

What’s the fun of being a bounty hunting droid if you don’t see that credit balance increase?
Here’s a route to fetch the credit balance for a droid, using the long route constraint.

/// <summary>
/// Long as a constraint
/// </summary>
/// <param name="creditBalance">an credit limit</param>
/// <returns>any droid with a given credit balance over given limit</returns>
[HttpGet("{creditBalance:long}", Order = 0)]
public IActionResult GetByCreditBalance(long creditBalance)
{
IEnumerable<Droid> droids = droidRepo.GetByCreditBalance(creditBalance);
if (droids == null || droids?.Count() == 0)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"No Droid found in database with a credit balance over {creditBalance}!"
}
);
}
return new OkObjectResult(droids);
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/api/droids/461168601842738790 | ConvertTo-Json
Gives the response:

{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-06T00:26:27.6872656Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
}

String Length Constraints

MinLength and MaxLength Route Constraint

In this example we are combining two route constraints. Both MinLength and MaxLength is used to specify an input between 2 to 4 characters that’s going to be used to search the specified droids armaments.

/// <summary>
/// Using a string with minlenght and maxlength to search for armaments
/// </summary>
/// <param name="droidId">droid id</param>
/// <param name="armament">armament search string</param>
/// <returns></returns>
[HttpGet("{droidId:int}/{armament:minlength(2):maxlength(4)}")]
public IActionResult GetSpecificArmament(int droidId, string armament)
{
var droid = droidRepo.Get(droidId);
if (droid == null)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"Droid with id: {droidId} - Not found in database!"
}
);
}

var matchingArmaments = droid.Armaments.Where(a => a.Contains(armament));
return new OkObjectResult(matchingArmaments);
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/api/droids/0/gas | ConvertTo-Json
Gives the response:

"Toxic gas dispensers"

Length Route Constraint

For the route to search for a specific droid by name, we’re using the length route constraint to specify that a droids name can be 5 charachters long – no more and no less.

/// <summary>
/// String of a specific length used as a constraint
/// </summary>
/// <param name="name">droid name</param>
/// <returns>an eventual droid matching the given name</returns>
[HttpGet("{name:length(5)}")]
public IActionResult Get(string name)
{
var droid = droidRepo.Get(name);
if (droid == null)
{
return new NotFoundObjectResult(
new Error.Repository.Error
{
HttpCode = 404,
Message = $"{name} - No such Droid in database!"
}
);
}
return new OkObjectResult(droidRepo.Get(name));
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/api/droids/IG-88 | ConvertTo-Json
Gives the response:

{
"id":  0,
"imperialContractId":  "0b450fdd-f484-423b-8685-4193e9fa583d",
"entryDate":  "2016-07-06T00:26:27.6872656Z",
"name":  "IG-88",
"creditBalance":  4611686018427387903,
"productSeries":  "IG-86",
"height":  1.96,
"armaments":  [
"DAS-430 Neural Inhibitor",
"Heavy pulse cannon",
"Poison darts",
"Toxic gas dispensers",
"Vibroblades"
],
"equipment":  [

]
}

Value Constraints

For the value constraints examples we have an endpoint that publishes a bunch of error descriptions. It has a range error codes matching internal errors, a range for external errors and a range for special errors.

Min Route Constraint

The min route constraint is used to specify the lower bounded value of the external errors range.

/// <summary>
/// min value specified for the constraint
/// </summary>
/// <param name="errorCode">error code to look for</param>
/// <returns>an external error</returns>
[HttpGet("{id:int:min(101)}", Order = 1)]
public IActionResult GetExternalErrors(int errorCode)
{
var error = errorRepository.GetByErrorCode(errorCode);
if (error == null)
{
return new BadRequestObjectResult(
new Error.Repository.Error
{
ErrorCode = 0,
HttpCode = 404,
Message = $"No external Error with error code {errorCode} exists in the database!"
});
}
return new OkObjectResult(error);
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/api/errors/101 | ConvertTo-Json
Gives the response:

{
"errorCode":  101,
"httpCode":  404,
"message":  "Too many fingers on keyboard error!"
}

Max Route Constraint

In this example a max value is set on the constraint to limit the range of the internal errors.

/// <summary>
/// max value specified for the contraint
/// </summary>
/// <param name="errorCode">error code to look for</param>
/// <returns>an internal error</returns>
[HttpGet("{id:int:max(100)}", Order = 2)]
public IActionResult GetInternalErrors(int errorCode)
{
var error = errorRepository.GetByErrorCode(errorCode);
if (error == null)
{
return new BadRequestObjectResult(
new Error.Repository.Error
{
ErrorCode = 0,
HttpCode = 404,
Message = $"No internal Error with error code {errorCode} exists in the database!"
});
}
return new OkObjectResult(error);
}

Calling the route from Postman with a get and using this URL:
http://localhost:5000/api/errors/99
Gives the following response (however note that this isn’t copied from a PowerShell invoke, since it returns a 400 – PowerShell refuses to produce proper output). Instead the response as taken from Postman looks like this:

{
"errorCode": 99,
"httpCode": 404,
"message": "No internal Error with error code 99 exists in the database!"
}

Range Route Constraint

And for the final value constraint we’re using the range keyword to specify a range of values available for special errors.

/// <summary>
/// range of values specified on the constraint
/// </summary>
/// <param name="errorCode">error code to look for</param>
/// <returns>a special error</returns>
[HttpGet("{id:int:range(665,667)}", Order = 0)]
public IActionResult GetSpecialErrors(int errorCode)
{
var error = errorRepository.GetByErrorCode(errorCode);
if (error == null)
{
return new BadRequestObjectResult(
new Error.Repository.Error
{
ErrorCode = errorCode,
HttpCode = 404,
Message = $"No special Error with error code {errorCode} exists in the database!"
});
}
return new OkObjectResult(error);
}

Calling the route from Postman with a get and using this URL:
http://localhost:5000/api/errors/665
Gives the response:

{
"errorCode": 665,
"httpCode": 404,
"message": "No special Error with error code 665 exists in the database!"
}

String Matching Constraints

Two constraints are string matching/regex based, these are the Alpha and the Regex constraints.
To demo them we have a simple Users controller, serving info about users of a system.

Alpha Route Constraint

The alpha constraint requires the letters a-z & A-Z.

/// <summary>
/// Alpha as a constraint
/// </summary>
/// <param name="handle">a users handle</param>
/// <returns>a user with the given handle</returns>
[HttpGet("{handle:alpha}", Order = 1)]
public IActionResult GetByHandle(string handle)
{
var user = usersRepository.GetUserByHandle(handle);
if (user == null)
{
return new BadRequestObjectResult(
new Error.Repository.Error
{
ErrorCode = 201,
HttpCode = 404,
Message = $"No User with handle {handle} exists in the database!"
});
}
return new OkObjectResult(user);
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/users/mobilemancer/ | ConvertTo-Json
Gives the response:

{
"handle":  "mobilemancer",
"id":  0,
"firstName":  "Andreas",
"lastName":  "W?nqvist",
"email":  "fake@mobilemancer.com"
}

Regex Route Constraint

This example is showing a practice of something that should never be done, sending personal information in the URL!
But as it’s just an example, here is a route with a very simple regex constraint that’s checking the format of the incoming parameter and making sure it resembles an email address.

/// <summary>
/// Regex as a constraint
/// </summary>
/// <param name="email">a users email address</param>
/// <returns>a user with the given email address</returns>
[HttpGet("{email:regex(^\\S+@\\S+$)}", Order = 2)]
public IActionResult GetByEmail(string email)
{
var user = usersRepository.GetUserByEmail(email);
if (user == null)
{
return new BadRequestObjectResult(
new Error.Repository.Error
{
ErrorCode = 202,
HttpCode = 404,
Message = $"No User with email {email} exists in the database!"
});
}
return new OkObjectResult(user);
}

Calling the route from PowerShell like this:
Invoke-RestMethod http://localhost:5000/users/fake@mobilemancer.com | ConvertTo-Json
Gives the response:

{
"handle":  "mobilemancer",
"id":  0,
"firstName":  "Andreas",
"lastName":  "W?nqvist",
"email":  "fake@mobilemancer.com"
}

Required Route Constraint

This constraint is supposedly used to mark any parameter in a route to be required. But I can’t think of any good example 🙁
If you can come up with a good example, let me know in the comments and it’ll end up here together with links to your twitter, blog or LinkedIn profile – whatever you prefer! 🙂

Get the Code

Checkout my GitHub repository if you want to take a closer look at any of the code used for this post, you can find it under the Core-WebAPI repo.

Happy Coding! 🙂


More ASP.NET Core Posts

How to create a Web API on ASP.NET Core with a minimal amount of code – Minimal Web API on ASP.NET Core
Quick setup of SSL for local dev with ASP.NET Core – ASP.Net Core – IIS Express and SSL

How to Master ASP.NET Core Web API Attribute Routing
Tagged on:         

6 thoughts on “How to Master ASP.NET Core Web API Attribute Routing

  • July 6, 2016 at 16:16
    Permalink

    Great article! Thank you. A few examples of POST, PUT and DELETE would be nice. Maybe a HttpPost with a user defined class as in parameter?

    • July 6, 2016 at 23:15
      Permalink

      Thanks 🙂

      Good suggestion, I’ll get on it as time permits.

      Happy coding!

      Edit: After giving it some thought, it won’t fit in with the theme and focus of this post that is Attribute Routing.
      However I might write another post about all the HTTP Methods available for Web API’s on ASP.NET Core, I think your suggestion would fit in well there.

  • Pingback: The week in .NET – 7/12/2016 | Tech News

  • July 18, 2016 at 10:27
    Permalink

    Very nice post! Really handy to have explicit examples of all the different constraints.

    There’s actually a couple of other constraints that come to mind which may be worth mentioning:

    Optional constraint using ‘?’
    Optional constraint with default values using ‘=’

    e.g.

    [HttpGet(“{echoValue?}”)]
    public string Get(string echoValue)
    {
    return echoValue ?? “NOT SPECIFIED”;
    }

    [HttpGet(“{echoValue=NOT SPECIFIED}”)]
    public string GetWithDefault(string echoValue)
    {
    return echoValue;
    }

    Wrt to the RequiredRouteConstraint, I agree with you – it doesn’t seem very useful with attribute routing. By definition, each segment of the attribute is always required, unless you explicitly make it optional. It looks like RequiredRouteConstraint is used internally to require an area is specified when using MapWebApiRoute, by I can’t see any way to use it with attribute routing.

    As an aside, I’m not sure whether the embedding of some of these constraints in the attribute route is a good idea. For constraints like min() and max() it seems like it may be better to have these as explicit pre-condition checks in the action itself. That way you can return 400 Bad Request or similar when the value is out of range, rather than a slightly confusing 404 NotFound. It just seems like it’s asking for difficulty down the line when you come to debugging routes not being hit… It’s good having all the tools, just have to be careful not to hurt yourself!

    • July 19, 2016 at 13:49
      Permalink

      Thanks for the great comment, Andrew!

      It’s funny, during the weekend I realized I had forgotten to mention optional constraints, so I was planning to add a section about it when I got time 🙂

      Regarding letting the Route Constraints be your only “error handling” I agree, it’s better to return a more suitable error with preferably more information in it than just a 404.
      However, I think the min, max, length Constraints can still be used successfully, you just have to supplement them with another fallback route for when they are not matching.
      I had originally planned to write about this in the post but got short on time, but I’ll make an addendum when time permits.

  • Pingback: How to: Build a Web API on ASP.NET Core for an Aurelia SPA - mobilemancer

Comments are closed.