Reputation: 26227
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
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
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
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
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
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