Optional URL Segments

Although the segment amount needs to match to the actual URL segment amount, you can customize this behavior with optional URL segments.

This can be done syntactically by providing a question mark to the segment in question:

...
app.UseMvc(routes => {
routes.MapRoute(name: "myroute", 
                template: "{controller=Home}/{action=Index}/{id?}"
};
...

So this route is matched by the following URLs and only matches a value to the id parameter if a value is provided:

  • /
    • controller: Home, action: Index
  • /Home
    • controller: Home, action: Index
  • /Home/Index
    • controller: Home, action: Index
  •  /Home/Index/Id
    • controller: Home, action: Index, id: Id
  • /Home/Index/Id/Segment
    • No match, to much segments

The above route is actually the DEFAULT MVC ROUTE! 

Another way to circumvent the conservatism of MVCs routing system is to define a catchall for the last segments that are potentially not supplied. This can be done by prefix a segment with an asterisk:

...
app.UseMvc(routes => {
routes.MapRoute(name: "myroute", 
                template: "{controller=Home}/{action=Index}/{id?}/{*catchall}"
};
...

You can name this segment whatever you want (except for the above mentioned 4 exceptions) but I choose catchall, because it is exactly what it does. This matches also the following URLs (additionally to the above mentioned):

  • /Home/Index/Id/All
    • controller: Home, action: Index, id: Id, catchall: All
  • /Home/Index/Id/All/Other
    • controller: Home, action: Index, id: Id, catchall: All/Other

This has no upper limit to the segments and can be useful for a quite liberal routing, or even testing purposes.

Routing constraints

As we saw in the last few sections we can control the degree of conservatism of the routing system. This works also in the other direction, which is constraining the liberalism of the routing system. This can be done with specifying C# types on the optional segments separated by a colon from the segment parameter name:

...
template: "{controller=Home}/{action=Index}/{id:int?}"
...

As you can see I specified the id to be of Type Nullable<int>. So the URL that match are constraint to have an Id segment that is either parseable to an integer or leaves out the segment at all.

This can also be specified by using a named argument to the MapRoute method of name constraints which is again an anonymous type with the name of the segment and a Type of the desired constraint, in this case:

...
app.UseMvc(routes => 
{
     routes.MapRoute(
         name: "myroute", 
         template: "{controller}/{action}/{id?}",
         defaults: new { controller = "Home", action = "Index" },
         constraints: new { id = new IntRouteConstraint() }
}; ...

For a comprehensive list of all types refer to MSDN documentation.

Regex, Type and Value Constraints

Regex as you well might anticipate allows for the most flexible route constraints. you specify a regex constraint by defining the following syntax:

...
app.UseMvc(routes => 
{
     routes.MapRoute(
         name: "myroute", 
         template: "{controller=Home}/{action=Index}/{id:regex(^A.*)=AllIds}"
}; ...

As you can see the regex constraint needs to specify regex(<expression>). This regex looks for an Id segment that is starts with an A and has an arbitrary number of additional characters.

The most constraints you will use are there to restrict segments to be converted to a specific Type or specific format. As we saw with the int constraint above. This can be extended to also only support a given range of values:

...
template: "{controller=Home}/{action=Index}/{id:range(10,20)?}"
...

This constraints the id to be in the range of 10 to 20 or left out entirely (question mark indicates optionality).

Combined and Custom Constraints

Besides defining the type of constraint, you also can combine constraints by simply chaining them together. Another way to define a constraint is to create your own. This can be done by implementing the IRouteConstraint interface. But I find the supplied constraints to be pretty comprehensive and will not look into this possibility in this post. Maybe I will write a post about this in the future in my aspnetcore/howto section.

Good to know for constraints

  • Defaults are applied before constraints are! So in the above examples if I request the root “/” always the defaults of Home for the controller and Index for the action are used.
  • Constraints are applied together, so if you have multiple constraints defined, they are either all satisfied or none is.

Inline vs. named arguments

We saw with defining routes that you can create all the constraints, default values and name of a route either inline or with named arguments that get supplied an anoynmous type with propertynames that match the expected values. Now the question might be which is the better choice. I think this comes down to personal preference, and I personally tend to use the inline alternative if possible and feasible as it is more concise.

Summary

In this post we looked at the basic features of the Asp.Net Core routing system. We looked at what task it fullfills in general, that is match URLs to controller actions and to generate outgoing URLs. In this post we concentrated on generating URL routes that match to a controller.

We explored how the patterns for URL work their matching to a controller. We also explored how to define segments with default values, how the conservatism of the amount of segments can be controlled and how we can restrict the liberalism to the values of each segment with constraints.

Further posts will entertain themselves with attribute routing, advanced features like generating outgoing URLs and creating custom constraints.


0 Comments

Leave a Reply