How mark a .NET Web API Regex Route Parameter as optional [duplicate] - c#

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?

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!

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.

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

should your regex be \d*?

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

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

Related

How to use a custom route getting value before {controller}/{action}/{id}

I need to have a variable before every controller.
I want something like that:
www.mysite.com/SYSTEMVARIABLE/Controller/Action
Example:
www.mysite.com/internalevent/Inscricao/Index
www.mysite.com/externalevent/Inscricao/Index
And i need to get the "internalevent" or "externalevent"
Example of values array
At this example, i need the [0] value of array, be the SYSTEMVARIABLE, not "Inscricao (Controller)"
So, how can i get the "SYSTEMVARIABLE" value and work with this ?
I have the following now (not working...)
routes.MapRoute(
name: "Default",
url: "{sistema}/{controller}/{action}/{id}",
defaults: new { controller = "Inscricao", action = "Index", id = UrlParameter.Optional },
constraints: new { sistema = new SistemaRouteConstraint() }
);
I need to get the value from "{sistema}", and i need to do some logic on this value.
Below is the class i want to get it, but ALWAYS the "{sistema}" only gets me the controller value (controller come twice on RouteValueDictionary)
public class SistemaRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
object sistema;
if(values.TryGetValue(parameterName, out sistema) && values != null)
{
var NomeSistema = values["sistema"].ToString();
using (Entities db = new Entities())
{
/*some logic here*/
}
}
return true;
}
}

Url routing like facebook asp.net

I am new to asp.net and currently learning url routing
In facebook link(https://www.facebook.com/james.wood) how does james.wood work?
I tried doing the same thing in my asp.net by using this code added in my global
route.MapPageRoute("Profile", "epubtest/profile/{profileid}", "~/epubtest/Profile.aspx");
But I cant make it work with dot like james.wood
Does the dot means another parameter or just one single parameter?
It works without a dot on it just one single word
1) You can add this route:
routes.MapRoute(
name: "User",
url: "{username}",
defaults: new { controller = "Destiny", action = "Index" },
constraints: new { username = new UserNameConstraint() }
);
2) Create this class:
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> users = new List<string>() { "username1", "username2" };
var username = values["username"].ToString().ToLower();
return users.Any(x => x.ToLower() == username);
}
}
3) DestinyController
public class DestinyController : Controller
{
public ActionResult Index(string username)
{
return View();
}
}
I hope I've helped.
Hugs!

How to fall back from a custom MvcRouteHandler to next one in Route Table

I have a custom MvcRouteHandler which checks database if a Url exists and pairs it with some controller action and Id.
However if this route handler can not find a matching pair in database, I'd like MVC to keep try with other defined route handlers in route table.
How can I do that?
Update: (Example code added)
routes.MapRoute(
name: "FriendlyRoute",
url: "{FriendlyUrl}").RouteHandler = new FriendlyRouteHandler();
FriendlyRouteHandler is:
public class FriendlyRouteHandler : MvcRouteHandler
{
private TancanDbContext db = new MyDbContext();
protected override IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
if (requestContext.RouteData.Values["FriendlyUrl"] != null)
{
string friendlyUrl = requestContext.RouteData.Values["FriendlyUrl"].ToString();
//Here, you would look up the URL Record in your database, then assign the values to Route Data
//using "where urlRecord.Url == friendlyUrl"
try
{
UrlRecord urlRecord = db.UrlRecords.Single(u => u.URL == friendlyUrl);
//Now, we can assign the values to routeData
if (urlRecord != null)
{
requestContext.RouteData.Values["controller"] = urlRecord.Controller;
requestContext.RouteData.Values["action"] = urlRecord.Action;
if(urlRecord.EntityId != null)
requestContext.RouteData.Values["id"] = urlRecord.ObjectId;
}
}
else
{
//Here, I want to redirect to next RouteHandler in route Table
requestContext.RouteData.Values["controller"] = friendlyUrl;
}
}
catch (Exception ex)
{
//throw;
//Here too, I want to redirect to next RouteHandler in route Table
requestContext.RouteData.Values["controller"] = friendlyUrl;
}
}
return base.GetHttpHandler(requestContext);
}
}
After adding this line it seems to work:
requestContext.RouteData.Values["controller"] = friendlyUrl;
Am I lucky or this is right way to do ? Do I need to use IRouteConstraint somewhere?
By the way, my influence was this article by Adam Riddick.
You want to use a custom Constraint, not a custom Handler for this.
routes.MapRoute(
name: "example",
url: "{friendly}",
defaults: new { controller = "FriendlyController", action = "Display" },
constraints: new { friendly = new FriendlyUrlConstraint() }
);
and then the constraint becomes:
public class FriendlyUrlConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var friendlyUrl = values[parameterName];
// Your logic to return true/false here
}
}

Preserve Case in Route Parameters with LowercaseUrls enabled

I am using routes.LowercaseUrls = true; in my MVC 4 application which is working great. However, parameters will also get lowercased, so if I have a route like
routes.MapRoute(
name: "MyController",
url: "foo/{hash}/{action}",
defaults: new { controller = "MyController", action = "Details" }
);
The link generated with
#Html.ActionLink("my link", "Details", new { hash=ViewBag.MyHash })
will have the {hash}-part of the URL lowercased as well, for example if ViewBag.MyHash = "aX3F5U" then the generated link will be /foo/ax3f5u instead of /foo/aX3F5U
Is there a way to force MVC to only lowercase the controller and action parts?
For older versions of MVC, the way to go seemed to be to implement a custom subclass of Route, however I don't know how/where to instantiate it, since the signature of the route constructors is quite different to MapRoute and I'm hoping there to be a simpler way.
I think the solution with a custom subclass of Route will be a good enough and simple, but at the same time a little bit ugly :)
You can add a CustomRoute at RegisterRoute method of RouteConfig.cs. Add the following code instead of routes.MapRoute
var route = new CustomRoute(new Route(
url: "{controller}/{action}/{id}",
defaults: new RouteValueDictionary() {
{ "controller", "Home" },
{ "action", "Index" },
{ "id", UrlParameter.Optional }
},
routeHandler: new MvcRouteHandler()
));
routes.Add(route);
Implementaion of particular CustomRoute may look like this:
public class CustomRoute : RouteBase
{
private readonly RouteBase route;
public CustomRoute(RouteBase route)
{
this.route = route;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values.Select(v =>
{
return v.Key.Equals("action") || v.Key.Equals("controller")
? new KeyValuePair<String, Object>(v.Key, (v.Value as String).ToLower())
: v;
}).ToDictionary(v => v.Key, v => v.Value));
return route.GetVirtualPath(requestContext, values);
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
return route.GetRouteData(httpContext);
}
}
However it's not an optimal implementation. A complete example could use a combination of extensions on RouteCollection and a custom Route child to keep it as close as possible to the original routes.MapRoute(...) syntax:
LowercaseRoute Class:
public class LowercaseRoute : Route
{
public LowercaseRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler) { }
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values.Select(v =>
{
return v.Key.Equals("action") || v.Key.Equals("controller")
? new KeyValuePair<String, Object>(v.Key, (v.Value as String).ToLower())
: v;
}).ToDictionary(v => v.Key, v => v.Value));
return base.GetVirtualPath(requestContext, values);
}
}
RouteCollectionExtensions Class:
public static class RouteCollectionExtensions
{
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
public static Route MapLowercaseRoute(this RouteCollection routes, string name, string url)
{
return MapLowercaseRoute(routes, name, url, null /* defaults */, (object)null /* constraints */);
}
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
public static Route MapLowercaseRoute(this RouteCollection routes, string name, string url, object defaults)
{
return MapLowercaseRoute(routes, name, url, defaults, (object)null /* constraints */);
}
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
public static Route MapLowercaseRoute(this RouteCollection routes, string name, string url, object defaults, object constraints)
{
return MapLowercaseRoute(routes, name, url, defaults, constraints, null /* namespaces */);
}
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
public static Route MapLowercaseRoute(this RouteCollection routes, string name, string url, string[] namespaces)
{
return MapLowercaseRoute(routes, name, url, null /* defaults */, null /* constraints */, namespaces);
}
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
public static Route MapLowercaseRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces)
{
return MapLowercaseRoute(routes, name, url, defaults, null /* constraints */, namespaces);
}
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
public static Route MapLowercaseRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
Route route = new LowercaseRoute(url, new MvcRouteHandler())
{
Defaults = CreateRouteValueDictionary(defaults),
Constraints = CreateRouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
};
if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, route);
return route;
}
private static RouteValueDictionary CreateRouteValueDictionary(object values)
{
var dictionary = values as IDictionary<string, object>;
if (dictionary != null)
{
return new RouteValueDictionary(dictionary);
}
return new RouteValueDictionary(values);
}
}
You can now use MapLowercaseRoute instead of MapRoute, so
routes.MapRoute(
name: "MyController",
url: "foo/{hash}/{action}",
defaults: new { controller = "MyController", action = "Details" }
);
simply becomes
routes.MapLowercaseRoute(
name: "MyController",
url: "foo/{hash}/{action}",
defaults: new { controller = "MyController", action = "Details" }
);
exposing the desired behaviour.
Here is one simple way to do this,
public class MyRoute : Route
{
public MyRoute(string url, object defaults): base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
if (values["action"] != null)
values["action"] = values["action"].ToString().ToLowerInvariant();
if (values["controller"] != null)
values["controller"] = values["controller"].ToString().ToLowerInvariant();
return base.GetVirtualPath(requestContext, values);
}
}
routes.Add("Default",new MyRoute("{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = MyUrlParameter.Optional }));
See this blog post for detail.
If you look at private RouteCollection.NormalizeVirtualPath method you'll see that it simply uses virtualPath.ToLowerInvariant(). So there is no way to handle that. Even if you create your own route it will be lowercased.
But what you can do is to add hash after '#' sign i.e. "foo/{action}/#{hash}". I haven't
tried, but it should work. Just look at NormalizeVirtualPath implementation.
It's simple as 1.2.3 ,
look at this example
routes.MapRouteLowercase( // changed from routes.MapRoute
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
It's simple to download and install it via Nuget, I use it.
PM> Install-Package LowercaseRoutesMVC
http://lowercaseroutesmvc.codeplex.com/

MVC 3 Route Constraints that check the POST

So I have a method that accepts some JSON data and binds the data to some variables. The route only has the method name in it, nothing else.
Is it possible to have a route constraint that looks at the POST data and checks some variables to determine whether it is the correct route?
The methods:
public ActionResult GetDataV1(string userId)
{
// Do stuff one way
}
public ActionResult GetDataV2(string userId)
{
// Do stuff another way
}
The routes:
routes.MapRoute(
"API GetData V1",
"GetData",
new { controller = "API", action = "GetDataV1" },
new { version = new APIVersionRoutingConstraint("1") }
);
routes.MapRoute(
"API GetData V2",
"GetData",
new { controller = "API", action = "GetDataV2" },
new { version = new APIVersionRoutingConstraint("2") }
);
The client would post { "version":"1", "userId":"mrjohn" } to /GetData and would get response from GetDataV1. The APIVersionRoutingConstraint would make sure the right method is called depending on the version variable.
Would it be good practice to try to deserialise the request stream inside the constraint? Maybe it would be better to put the version in the URL like /GetData/1 and the other variables in the JSON body?
Rather than trying to check the version as a route constraint, why not go to one Action that then checks the version to execute the appropriate work?
I haven't tested this, but the idea is:
public ActionResult GetData(string userId, int version)
{
switch(version)
{
case 1:
return GetDataV1(userId);
case 2:
return GetDataV2(userId);
// You could always add more cases here as you get more versions,
// and still only have the one route
default:
// Probably more appropriate to return a result that contains error info,
// but you get the idea
throw new ArgumentOutOfRangeException("version");
}
}
// Made these private since they are called from the public action
private ActionResult GetDataV1(string userId)
{
// Do stuff one way
}
private ActionResult GetDataV2(string userId)
{
// Do stuff another way
}
And then you only need the one route
routes.MapRoute(
"GetData",
"GetData",
new { controller = "API", action = "GetData" },
new { version = "\d+" } // Require a numeric version
);
Made the APIVersionRoutingConstraint like this:
public class APIVersionRoutingConstraint : IRouteConstraint
{
private string _versionNumber;
public APIVersionRoutingConstraint(string versionNumber)
{
_versionNumber = versionNumber;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string version = null;
if (httpContext.Request.ContentType.Contains("application/json"))
{
string body = new StreamReader(httpContext.Request.InputStream).ReadToEnd();
httpContext.Request.InputStream.Position = 0;
var vals = new JavaScriptSerializer().DeserializeObject(body) as Dictionary<string, object>;
if (vals.ContainsKey("version"))
version = vals["version"].ToString();
}
// Must have version to pass constraint
return !string.IsNullOrEmpty(version) && version == _versionNumber;
}
}
It's not super-efficient I guess, but it gets the job done. Only the route that matches the version in the POST body gets used.

Categories

Resources