matthewrk matthewrk - 9 days ago 6x
ASP.NET (C#) Question

ASP.NET Core Route Attributes - ID and hyphenated slug separated by a hyphen

I'm trying to configure the following url structure for one of my controller actions via the Route attribute:


Here's my route as it stands:

public ContentResult Show(int id, string slug)

This doesn't match the intended route, but it does match:


and also matches with a trailing hyphen after one word, as soon as I add anything else it doesn't match.

Interestingly if I swap out the string literal hyphen (not the regex ones) for a /, it works fine in its entirety:

public ContentResult Show(int id, string slug)

successfully matching:


So it seems to be tripping on the string literal hyphen. Any ideas?


If you dig under the hood, you will find that the Routing middleware is greedily splitting complex route segments like {id:int}-{name:regex([[\w\-]]+)} even before applying the route constraints. (Happens both using route attributes and the route table in Startup)

This means:

  • With url like products/123-foo, the route matches 123 as id and foo as name. It will then apply the constraints, finding a match as 123 is a valid int and foo matches the regex.
  • With url like products/123-foo-, the route matches 123 as id and foo- as name. It will then apply the constraints, finding a match again.
  • With url like products/123-foo-bar, the route matches 123-foo as id and bar as name. It will then apply the constraints, but this time it will fail as 123-foo is not a valid int!

You don't have this issue if you split the parameters in different route segments as in {id:int}/{name:regex([[\w\-]]+)}, as the / will split the parameters right as you would expect them to.

If your route really needs to have that shape, I would then use a single parameter in the route constraint. This parameter would wrap both the id and the name:


The problem is that you would then need to manually extract the id and name from that single parameter.

  • You could manually do that inside the controller action. For a one off this might be acceptable
  • You could create an ActionFilter and split the combined route parameter into action parameters before the action is executed (overriding OnActionExecuting). This still quite hacky, specially my quick and dirty version:

    public class SplitProductParametersActionFilter : ActionFilterAttribute
        private static Regex combinedRegex = new Regex(@"^([\d]+)-([\w\-]+)$");
        public override void OnActionExecuting(ActionExecutingContext context)
            var combined = context.RouteData.Values["combined"].ToString();
            var match = combinedRegex.Match(combined);
            if (match.Success)
                context.ActionArguments.Add("id", int.Parse(match.Groups[1].Value));
                context.ActionArguments.Add("name", match.Groups[2].Value);
    public IActionResult Contact(int id, string name)
  • You could create a new model binder with its model binder provider and some annotation attribute for your parameters. This might be the cleanest as it is similar to the approach above but extended MVC in the expected way regarding model binding, however I havent had time to explore it:

    public IActionResult Contact([FromUrlProduct("combined")]int id, [FromUrlProduct("combined")]string name)

In order to debug the route constraints, you can set the logging as debugging and you should see a message like this in the console (you might need to run the app from the console with dotnet run instead of using ISS from VS):

dbug: Microsoft.AspNetCore.Routing.RouteConstraintMatcher[1]
      => RequestId:0HKVJG96H1RQE RequestPath:/products/1-foo-bar
      Route value '1-foo' with key 'id' did not match the constraint 'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint'.

You can also manually copy the int route constraint and registered it in Startup after services.AddMvc() with services.Configure<RouteOptions>(opts => opts.ConstraintMap.Add("customint", typeof(CustomIntRouteConstraint)))

An approach like the one described in this blog might also help with debugging.