TOC

The community is working on translating this tutorial into Polish, but it seems that no one has started the translation process for this article yet. If you can help us, then please click "More info".

Routing:

Routing Constraints

In the previous articles of this chapter, we saw how we could create very flexible and thereby broad-matching route templates, to catch many different kind of URL structures and map them correctly. However, at some point, your routes may become match too broadly or perhaps you just have a very specific structure that you want to enforce. For this, ASP.NET MVC has introduced the clever concept of route constraints, allowing you to be much more specific when defining the parameters of your routes.

To illustrate how routing constraints can help, let's take a look at the example from the previous article, where we created a route for handling a classic ID/UrlSlug scenario:

[Route("blog/{entryId}/{slug}")]
public IActionResult Blog(int entryId, string slug)
{
    return Content($"Blog entry with ID #{entryId} requested (URL Slug: {slug})");
}

There are several types of route constraints, so let's use this example to see how they all work.

Data type constraints

In this case, we use an integer for the entryId parameter, so we might as well enforce that directly in the template, using the special route constraint syntax:

[Route("blog/{entryId:int}/{slug}")]  
public IActionResult Blog(int entryId, string slug)  
{  
    ....

The difference can be found in the , where I have added :int, like this: {entryId:int}. Now the URL will only match the route if the entryId parameter is an integer, like this:

/blog/153/testing-the-system/

URL's like this one will no longer match:

/blog/abc153def/testing-the-system/

Data type constraint types

The int data type constraint is of course just one out of many. Here are examples of other data type constraints which you can use in your routes:

  • {entryId:int}
  • {isVisible:bool}
  • {entryDate:datetime}
  • {weight:double}
  • {weight:float}
  • {price:decimal}
  • {id:guid}
  • {postId:long}

Length/range constraints

Another type of route constraints will help you enforce a certain minimum and/or maximum length for strings, or a minimum/maximum value for numeric types. We could use that to make our route template from before even more specific. For instance, since the entryId should likely match a database entry, we could argue that the value should be higher than 0. We can use the min constraint to do that:

[Route("blog/{entryId:min(1)}/{slug}")]  
public IActionResult Blog(int entryId, string slug)  
{  
    ....

If we know there's a maximum value, we can use the max constraint, which works just like the min constraint. or, if there's both a minimum and a maximum value, we can use the range constraint:

[Route("blog/{entryId:range(1, 999999)}/{slug}")]
public IActionResult Blog(int entryId, string slug)
{
    ....

We also want to prevent empty strings for the slug parameter - in fact, since the slug is likely based on the title of the blog post in our scenario, we might argue that the slug should never be shorter than 3 characters. We can use the minlength constraint for this:

[Route("blog/{entryId:range(1, 999999)}/{slug:minlength(3)}")]
public IActionResult Blog(int entryId, string slug)
{
    ....

In case you need it, there's of course also a maxlength constraint and even a length constraint - the first one specifies a maximum length, while the latter enforces a string of a specific length, e.g. length(8) to only accept a string of exactly 8 characters.

Combining multiple constraints

With all the above in mind, I believe that now is a good time to illustrate an important detail about route constraints: They can easily be combined. For instance, you may require both a specific data type as well as a minimum length, or you may even string together multiple length constraints. You simply separate each constraint with a : (colon) character, like this:

[Route("blog/{entryId:int:range(1, 999999)}/{slug:minlength(3):maxlength(50)}")]
public IActionResult Blog(int entryId, string slug)
{
    ....

With this in place, the first parameter has to be of the integer data type and must be between 1 and 999999. The second parameter defaults to a string, since no data type is specified, and it has to be at least 3 characters long but no longer than 50 characters.

Regular Expression constraints

When you need even more control over how accepted parameters can look, you can use Regular Expressions. They allow you to specify a very precise pattern, using all the extremely powerful text matching tools found in the Regular Expression syntax. In fact, Regular Expressions are a huge topic in it self, so the syntax won't be covered in this article, but I will of course show you how you can use a Regular Expression in an ASP.NET MVC route constraint.

Inspired by our URL slug example used heavily in the examples above, we might decide that our entry ID and slug-part should not be separate parameters - instead, we want them combined in a more natural-looking string, allowing for URL's like this one:

/blog/153-testing-the-system/

We still want the first part to be an integer, allowing us to find the desired post in the database from the ID, so we create a Regular Expression to match this type of URL:

^[0-9]{1,7}\-[a-z0-9\-]{3,50}$

If you don't know Regular Expressions, this probably look like gibberish, but stick with me anyway. According to this Regex, we need a string that starts with a number, with a minimum length of 1 and a maximum length of 7 (length of the matched string, e.g. 9999999). After that, a hyphen should occur and then a string consisting of letters, numbers and hyphens only, with a minimum length of 3 and a maximum length of 50. We can now apply this to our route, using the regex() constraint:

[Route(@"blog/{slug:regex(^[[0-9]]{{1,7}}\-[[a-z0-9\-]]{{3,50}}$)}")]    
public IActionResult Blog(string slug)    
{    
    ....

You will notice that the Regex part looks a bit differently than before. Because the route constraint syntax uses special characters like { and [, which also has special meaning in a Regular Expression, they are escaped by doubling them.

Now, in your action method, you can use simple string splitting techniques to separate the ID part from the slug part and then fetch and display the correct post. Here's an example:

[Route(@"blog/{slug:regex(^[[0-9]]{{1,7}}\-[[a-z0-9\-]]{{3,50}}$)}")]  
public IActionResult Blog(string slug)  
{  
    int hyphenPos = slug.IndexOf("-");  
    int entryId = int.Parse(slug.Substring(0, hyphenPos));  
    string slugPart = slug.Substring(hyphenPos + 1);  
    return Content($"Blog entry with ID #{entryId} requested (URL Slug: {slug})");  
}

So, by using the power of Regular Expressions, you can enforce even the most complex URL patterns for your routes.

Summary

By applying constraints to your routes you get more control over when a URL matches a specific route. This is especially useful if you have several similar routes, which could interfere with each other. By applying route constraints to one or several of these routes you get more control over which route is matched for various URL's.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!