Adding a Custom Inline Route Constraint in ASP.NET Core 1.0
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.
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.
About Newsletter
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.
Brian - there's a fine line between validation (is this correct, is it valid) and validation ;) (should this fire the route)
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.
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.
[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...
Comments are closed.
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 :)