24

I'm trying to write a route with a nullable int in it. It should be possible to go to both /profile/ but also /profile/\d+.

routes.MapRoute("ProfileDetails", "profile/{userId}",
                new {controller = "Profile",
                     action = "Details",
                     userId = UrlParameter.Optional},
                new {userId = @"\d+"});

As you can see, I say that userId is optional but also that it should match the regular expression \d+. This does not work and I see why.

But how would I construct a route that matches just /profile/ but also /profile/ followed by a number?

Deniz Dogan
  • 25,711
  • 35
  • 110
  • 162

6 Answers6

29

The simplest way would be to just add another route without the userId parameter, so you have a fallback:

routes.MapRoute("ProfileDetails", "profile/{userId}",
                new {controller = "Profile",
                     action = "Details",
                     userId = UrlParameter.Optional},
                new {userId = @"\d+"});

routes.MapRoute("Profile", "profile",
                new {controller = "Profile",
                     action = "Details"});

As far as I know, the only other way you can do this would be with a custom constraint. So your route would become:

routes.MapRoute("ProfileDetails", "profile/{userId}",
                new {controller = "Profile",
                     action = "Details",
                     userId = UrlParameter.Optional},
                new {userId = new NullableConstraint());

And the custom constraint code will look like this:

using System;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;

namespace YourNamespace
{
    public class NullableConstraint : IRouteConstraint
    {
        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (routeDirection == RouteDirection.IncomingRequest && parameterName == "userId")
            {
                // If the userId param is empty (weird way of checking, I know)
                if (values["userId"] == UrlParameter.Optional)
                    return true;

                // If the userId param is an int
                int id;
                if (Int32.TryParse(values["userId"].ToString(), out id))
                    return true;
            }

            return false;
        }
    }
}

I don't know that NullableConstraint is the best name here, but that's up to you!

Mark Bell
  • 28,985
  • 26
  • 118
  • 145
  • I made something similar to this, but slightly more generic, with a constructor that takes a regular expression to match against the provided value. Thanks! – Deniz Dogan Oct 05 '10 at 10:41
  • I know I'm coming in really late here, but shouldn't that last line be return true? Otherwise the constraint always fails if the route direction is UrlGeneration. – Ben Mills Feb 04 '13 at 22:03
  • If the last line returned true then *anything* would match the constraint... but yes, you're right, this doesn't take into account anything other than incoming request routing. Looking at it now, I would have thought we could just remove the routeDirection part of the `if` statement altogether; I'll test tomorrow and edit if that is the case. – Mark Bell Feb 05 '13 at 19:34
  • Maybe the assumption with the route direction code is that we trust that the parameter is going to be valid for URL generation (because the developer supplying the value). – Ben Mills Feb 06 '13 at 14:54
  • using `int? id` will allow nullable int, check [here](http://stackoverflow.com/questions/5436031/asp-net-mvc-c-sharp-routing-passing-a-null-integer), hope helps. – Shaiju T Nov 11 '15 at 10:33
13

It's possible something changed since this question was answered but I was able to change this:

routes.MapPageRoute(
    null,
    "projects/{operation}/{id}",
    "~/Projects/ProjectWizard.aspx",
    true,
    new RouteValueDictionary(new
    {
        operation = "new",
        id = UrlParameter.Optional
    }),
    new RouteValueDictionary(new
    {
        id = new NullableExpressionConstraint(@"\d+")
    })
);

With this:

routes.MapPageRoute(
    null,
    "projects/{operation}/{id}",
    "~/Projects/ProjectWizard.aspx",
    true,
    new RouteValueDictionary(new
    {
        operation = "new",
        id = UrlParameter.Optional
    }),
    new RouteValueDictionary(new
    {
        id = @"\d*"
    })
);

Simply using the * instead of the + in the regular expression accomplished the same task. The route still fired if the parameter was not included, but if included it would only fire if the value was a valid integer. Otherwise it would fail.

CatDadCode
  • 58,507
  • 61
  • 212
  • 318
7

ASP.NET MVC 3 has solved this problem, and as Alex Ford brought out, you can use \d* instead of writing a custom constraint. If your pattern is more complicated, like looking for a year with \d{4}, just make sure your pattern matches what you want as well as an empty string, like (\d{4})? or \d{4}|^$. Whatever makes you happy.

If you are still using ASP.NET MVC 2 and want to use Mark Bell's example or NYCChris' example, please be aware that the route will match as long as the URL parameter contains a match to your pattern. This means that the pattern \d+ will match parameters like abc123def. To avoid this, wrap the pattern with ^( and )$ either when defining your routes or in the custom constraint. (If you look at System.Web.Routing.Route.ProcessConstraint in Reflector, you'll see that it does this for you when using the built in constraint. It also sets the CultureInvariant, Compiled, and IgnoreCase options.)

Since I already wrote my own custom constraint with the default behavior mentioned above before realizing I didn't have to use it, I'll leave it here:

public class OptionalConstraint : IRouteConstraint
{
  public OptionalConstraint(Regex regex)
  {
    this.Regex = regex;
  }

  public OptionalConstraint(string pattern) :
    this(new Regex("^(" + pattern + ")$",
      RegexOptions.CultureInvariant |
      RegexOptions.Compiled |
      RegexOptions.IgnoreCase)) { }

  public Regex Regex { get; set; }

  public bool Match(HttpContextBase httpContext,
                    Route route,
                    string parameterName,
                    RouteValueDictionary values,
                    RouteDirection routeDirection)
  {
    if(routeDirection == RouteDirection.IncomingRequest)
    {
      object value = values[parameterName];
      if(value == UrlParameter.Optional)
        return true;
      if(this.Regex.IsMatch(value.ToString()))
        return true;
    }

    return false;
  }
}

And here's an example route:

routes.MapRoute("PostsByDate",
                "{year}/{month}",
                new { controller = "Posts",
                      action = "ByDate",
                      month = UrlParameter.Optional },
                new { year = @"\d{4}",
                      month = new OptionalConstraint(@"\d\d") });
Community
  • 1
  • 1
jordanbtucker
  • 5,768
  • 2
  • 30
  • 43
3

should your regex be \d*?

Anthony Johnston
  • 9,405
  • 4
  • 46
  • 57
  • With `\d*` it doesn't match at all, be it with or without `UrlParameter.Optional`. – Deniz Dogan Oct 05 '10 at 09:51
  • 3
    @DenizDogan Not sure if something changed since then but I just tested `\d*` and it accomplished the exact same thing as the `OptionalRegExConstraint`. – CatDadCode May 07 '12 at 22:45
3

Thanks to Mark Bell for this answer, it helped me quite a bit.

I'm wondering why you hard coded the check for "userId" in the constraint? I slightly rewrote your class like to user the parameterName parameter, and it seems to be working just fine.

Am I missing anything by doing it this way?

public class OptionalRegExConstraint : IRouteConstraint
{
    private readonly Regex _regEx;

    public OptionalRegExConstraint(string matchExpression=null)
    {
        if (!string.IsNullOrEmpty(matchExpression))
            _regEx = new Regex(matchExpression);
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest)
        {
            if (values[parameterName] == UrlParameter.Optional) return true;

            return _regEx != null && _regEx.Match(values[parameterName].ToString()).Success;
        }
        return false;
    }
}
NYCChris
  • 649
  • 5
  • 13
  • I hard-coded that value only for the sake of obviousness in the example, but you're right—this approach is more flexible than mine. +1 – Mark Bell Mar 09 '12 at 09:17
0

I needed to validate a few things with more than just a RegEx but was still getting an issue similar to this. My approach was to write a constraint wrapper for any custom route constraints I may already have:

public class OptionalRouteConstraint : IRouteConstraint
{
    public IRouteConstraint Constraint { get; set; }

    public bool Match
        (
            HttpContextBase httpContext,
            Route route,
            string parameterName,
            RouteValueDictionary values,
            RouteDirection routeDirection
        )
    {
        var value = values[parameterName];

        if (value != UrlParameter.Optional)
        {
            return Constraint.Match(httpContext, route, parameterName, values, routeDirection);
        }
        else
        {
            return true;
        }
    }
}

And then, in constraints under a route in RouteConfig.cs, it would look like this:

defaults: new {
    //... other params
    userid = UrlParameter.Optional
}
constraints: new
{
    //... other constraints
    userid = new OptionalRouteConstraint { Constraint = new UserIdConstraint() }
}
SomeShinyObject
  • 7,581
  • 6
  • 39
  • 59