Deniz Dogan
Deniz Dogan

Reputation: 26227

ASP.NET MVC: Route with optional parameter, but if supplied, must match \d+

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?

Upvotes: 24

Views: 28689

Answers (6)

SomeShinyObject
SomeShinyObject

Reputation: 7821

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() }
}

Upvotes: 0

jordanbtucker
jordanbtucker

Reputation: 6088

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") });

Upvotes: 7

ChevCast
ChevCast

Reputation: 59234

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.

Upvotes: 13

NYCChris
NYCChris

Reputation: 649

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;
    }
}

Upvotes: 3

Mark Bell
Mark Bell

Reputation: 29785

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!

Upvotes: 29

Anthony Johnston
Anthony Johnston

Reputation: 9604

should your regex be \d*?

Upvotes: 3

Related Questions