Scott Hanselman

Adding a Custom Inline Route Constraint in ASP.NET Core 1.0

June 23, '16 Comments [14] Posted in ASP.NET | ASP.NET MVC
Sponsored By

ASP.NET supports both attribute routing as well as centralized routes. That means that you can decorate your Controller Methods with your routes if you like, or you can map routes all in one place.

Here's an attribute route as an example:

[Route("home/about")]
public IActionResult About()
{
//..
}

And here's one that is centralized. This might be in Startup.cs or wherever you collect your routes. Yes, there are better examples, but you get the idea. You can read about the fundamentals of ASP.NET Core Routing in the docs.

routes.MapRoute("about", "home/about",
new { controller = "Home", action = "About" });

A really nice feature of routing in ASP.NET Core is inline route constraints. Useful URLs contain more than just paths, they have identifiers, parameters, etc. As with all user input you want to limit or constrain those inputs. You want to catch any bad input as early on as possible. Ideally the route won't even "fire" if the URL doesn't match.

For example, you can create a route like

files/{filename}.{ext?}

This route matches a filename or an optional extension.

Perhaps you want a dateTime in the URL, you can make a route like:

person/{dob:datetime}

Or perhaps a Regular Expression for a Social Security Number like this (although it's stupid to put a SSN in the URL ;) ):

user/{ssn:regex(d{3}-d{2}-d{4})}

There is a whole table of constraint names you can use to very easily limit your routes. Constraints are more than just types like dateTime or int, you can also do min(value) or range(min, max).

However, the real power and convenience happens with Custom Inline Route Constraints. You can define your own, name them, and reuse them.

Lets say my application has some custom identifier scheme with IDs like:

/product/abc123

/product/xyz456

Here we see three alphanumerics and three numbers. We could create a route like this using a regular expression, of course, or we could create a new class called CustomIdRouteConstraint that encapsulates this logic. Maybe the logic needs to be more complex than a RegEx. Your class can do whatever it needs to.

Because ASP.NET Core is open source, you can read the code for all the included ASP.NET Core Route Constraints on GitHub. Marius Schultz has a great blog post on inline route constraints as well.

Here's how you'd make a quick and easy {customid} constraint and register it. I'm doing the easiest thing by deriving from RegexRouteConstraint, but again, I could choose another base class if I wanted, or do the matching manually.

namespace WebApplicationBasic
{
public class CustomIdRouteConstraint : RegexRouteConstraint
{
public CustomIdRouteConstraint() : base(@"([A-Za-z]{3})([0-9]{3})$")
{
}
}
}

In your ConfigureServices in your Startup.cs you just configure the route options and map a string like "customid" with your new type like CustomIdRouteConstraint.

public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.Configure<RouteOptions>(options =>
options.ConstraintMap.Add("customid", typeof(CustomIdRouteConstraint)));
}

Once that's done, my app knows about "customid" so I can use it in my Controllers in an inline route like this:

[Route("home/about/{id:customid}")]
public IActionResult About(string customid)
{
// ...
return View();
}

If I request /Home/About/abc123 it matches and I get a page. If I tried /Home/About/999asd I would get a 404! This is ideal because it compartmentalizes the validation. The controller doesn't need to sweat it. If you create an effective route with an effective constraint you can rest assured that the Controller Action method will never get called unless the route matches.

If the route doesn't fire it's a 404

Unit Testing Custom Inline Route Constraints

You can unit test your custom inline route constraints as well. Again, take a look at the source code for how ASP.NET Core tests its own constraints. There is a class called ConstrainsTestHelper that you can borrow/steal.

I make a separate project and setup xUnit and the xUnit runner so I can call "dotnet test."

Here's my tests that include all my "Theory" attributes as I test multiple things using xUnit with a single test. Note we're using Moq to mock the HttpContext.

public class TestProgram
{

[Theory]
[InlineData("abc123", true)]
[InlineData("xyz456", true)]
[InlineData("abcdef", false)]
[InlineData("totallywontwork", false)]
[InlineData("123456", false)]
[InlineData("abc1234", false)]
public void TestMyCustomIDRoute(
string parameterValue,
bool expected)
{
// Arrange
var constraint = new CustomIdRouteConstraint();

// Act
var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);

// Assert
Assert.Equal(expected, actual);
}
}

public class ConstraintsTestHelper
{
public static bool TestConstraint(IRouteConstraint constraint, object value,
Action<IRouter> routeConfig = null)
{
var context = new Mock<HttpContext>();

var route = new RouteCollection();

if (routeConfig != null)
{
routeConfig(route);
}

var parameterName = "fake";
var values = new RouteValueDictionary() { { parameterName, value } };
var routeDirection = RouteDirection.IncomingRequest;
return constraint.Match(context.Object, route, parameterName, values, routeDirection);
}
}

Now note the output as I run "dotnet test". One test with six results. Now I'm successfully testing my custom inline route constraint, as a unit. in isolation.

xUnit.net .NET CLI test runner (64-bit .NET Core win10-x64)
Discovering: CustomIdRouteConstraint.Test
Discovered: CustomIdRouteConstraint.Test
Starting: CustomIdRouteConstraint.Test
Finished: CustomIdRouteConstraint.Test
=== TEST EXECUTION SUMMARY ===
CustomIdRouteConstraint.Test Total: 6, Errors: 0, Failed: 0, Skipped: 0, Time: 0.328s

Lots of fun!


Sponsor: Working with DOC, XLS, PDF or other business files in your applications? Aspose.Total Product Family contains robust APIs that give you everything you need to create, manipulate and convert business files along with many other formats in your applications. Stop struggling with multiple vendors and get everything you need in one place with Aspose.Total Product Family. Start a free trial today.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb
Thursday, 23 June 2016 19:43:57 UTC
Looks like you needs some PII Sensitivity training. Never ever pass DOB or SSN in a URL. No not even then.

https://en.wikipedia.org/wiki/Personally_identifiable_information

I know you were just trying to illustrate a point. Maybe you have a pool on how long it would take for someone to notice this issue :)
Joshua Kincaid
Thursday, 23 June 2016 19:46:01 UTC
On a .NET 4.5 project, was just thinking a week or two ago about how nice route constraints would be. Looks good!
Thursday, 23 June 2016 19:56:27 UTC
From the documentation linked to in the article:

Avoid using constraints for validation, because doing so means that invalid input will result in a 404 (Not Found) instead of a 400 with an appropriate error message. Route constraints should be used to disambiguate between routes, not validate the inputs for a particular route.


Not sure yet what I think about that.
Thursday, 23 June 2016 19:58:55 UTC
Joshua - I'm pretty sure I literally said it was a bad idea a few words later.

Brian - there's a fine line between validation (is this correct, is it valid) and validation ;) (should this fire the route)
Scott Hanselman
Thursday, 23 June 2016 21:08:46 UTC
You could use "phone number" instead of SSN to show the same thing :)
Peter
Friday, 24 June 2016 15:31:15 UTC
Brian - this isn't about validating what is being sent, it is making sure the route doesn't fire if the data is bad. You should still validate the data within the action if necessary (prevent aaa000 if that is not a valid value). And you can create a custom filter and error page to map to the 404 so you get better information (that the error was related to the route and not just a bad URL like /foo/bar).
Saturday, 25 June 2016 05:53:27 UTC
Great post!

For single-page apps it might be a good idea to have cross-platform routes that could be universally used in both client-side (JavaScript app) and server-side (.NET) code. I'm going to publish a working example here soon - ASP.NET Core Starter Kit (see client/routes.json). Scott's example with unit testing will perfectly work there as well.
Saturday, 25 June 2016 11:29:38 UTC
Great Article! Thanks for sharing :)
Sunday, 26 June 2016 06:21:06 UTC
Thursday, 30 June 2016 19:01:17 UTC
I want to create hotmail login .can any one help me
Friday, 01 July 2016 05:27:53 UTC
This is really awesome.
Dark Shadow
Wednesday, 06 July 2016 02:10:55 UTC
Hello,

I'm trying to build a REST API and match this particular URL: /api/Databases/123/Images/789

So I have a Databases controller which catch HTTP requests like /api/Databases/123 but I also have an Images controller.

Is there a way for my ImagesController to handle this kind of URL and maybe get the Databases ID in its constructor or something like this? Or my only way is to define a new route in my DatabasesController?

The thing is that following REST principle, this endpoint could grows up pretty big, adding more and more methods to my DatabasesController. For example imagine this URL: /api/Databases/123/Images/456/Foo/789/NestedAndNested/098 if my controller accepts GET, POST, PUT, DELETE and even PATCH for each URL "level" then it would become pretty big...

Thanks for you answer, I don't see the best clean way to handle this.
Wednesday, 06 July 2016 03:18:57 UTC
I finally managed to do it making my ImagesController inherits of DatabasesController


[Route("api/Databases")]
public class ImagesController : DatabasesController
{
[HttpGet("{databaseId:int}/[controller]/{imageId:int}")]
public string Get(int databaseId, int imageId)
{
return string.Format("databaseId:{0}, imageId:{1}", databaseId, imageId);
}
}


I'm not able to find the list of parameters the MVC Route attribute can accept but it would be nice to have something like [Route("[BaseController]") or [Route("[controller:base]")

PS: Sorry for the off-topic, you are talking here about constraints and I'm not...
Tuesday, 26 July 2016 08:45:48 UTC
Hi, Nice update on Asp.net Core 1.0. Here describes how to add a custom inline route constraint. A really nice feature of routing in ASP.NET Core is inline route constraints. Here in this blog so much useful information available to know. I had a little bit idea about Asp.net when I hosted my business through Myasp.net. Thanks a lot for sharing with us.
smithcole
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.