ASP.NET MVC 3 Route Constraint : Regular Expression for not null - asp.net

I am trying to create a route constraint but not sure what would be the best for this. Here is the route with no constraint :
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty" },
new { tag = #"" } //Here I would like to put a RegEx for not null match
);
What would be the best solution for this?

Could you create an IRouteConstraint:
public class NotNullRouteConstraint : IRouteConstraint
{
public bool Match(
HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
return (values[parameterName] != null);
}
}
Which you can wire up:
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty" },
new { tag = new NotNullRouteConstraint() }
);

Why do you need a constraint for a not null/empty match? Normally if you define your route like this:
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty" },
);
and tag is not specified in the request url this route simply won't match.
And if you want a token to be optional, then:
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty", tag = UrlParameter.Optional },
);
Constraints are used when you want to constrain the value of a given route token to some specific format.

At first, I tried to create a RegEx for empty string which is ^$ (which is what null would be). However, it doesn't look like route constraints can be !=. How about matching one or more character with ^.+$?
So:
tag = #"^.+$"

Related

Html.Routelink, Html.BeginRouteForm, Ajax.RouteLink and Ajax.BeginRouteForm not working

RouteLink and BeginRouteForm gives empty href. If I try to remove any of the custom routes defined in my code, it gives a compilation error. I tried looking for answers in similar questions but none of them works.
My routes contain optional parameters like this:
routes.MapRoute("CustomRoute", "Double/Steps/Add/{id}", defaults: new { controller = "DoubleDodge", action = "Step" });
This is how I am calling the route:
<div>
#Ajax.RouteLink("Dodge Me", "CustomRoute", new { id = ViewBag.Id }, new AjaxOptions { AllowCache = false, UpdateTargetId = "main-content-div", InsertionMode = InsertionMode.Replace })
</div>
Looking forward for your response. Thanks :)
Using Routelink, you will get a empty href if MVC cannot resolve your route as specified.
I'm betting that your CustomRoute is below your Default route config. Make sure that any specialized routes are at the top so MVC matches those first, as it will return the 1st match. And MVC is probably looking for a "Double" controller, "Steps" action, and an "Add" id value.
If that's not the case, then the same issue could happen if your ViewBag.Id doesn't contain a value, especially since your "id" isn't optional in your route. You could make that optional by adding UrlParameter.Optional like so:
routes.MapRoute(
name: "CustomRoute",
url: "Double/Steps/Add/{id}",
defaults: new { controller = "DoubleDodge", action = "Step", id = UrlParameter.Optional }
);
And I assume your Controller/Action looks like this:
public class DoubleDodge : Controller
{
public JsonResult Step(string id)
{
// create result
MyViewModel vm = new MyViewModel();
return Json(vm);
}
}

What is the method for generating urls using RouteLink when a route name is not supplied?

Using #Html.RouteLink with a route name simply directs the routing engine to use the route of that name. This is easy to understand and also apparently the only way anyone who writes a blog entry or the documentation attempts to explain RouteLink.
I appreciate that this is the preferred method and I too have always used a route name with RouteLink but I want to know how it works when we don't and I cant find any really good info on it. So the method is question is:
public static MvcHtmlString RouteLink(
this HtmlHelper htmlHelper,
string linkText,
RouteValueDictionary routeValues
)
and in keeping with all the interesting stuff on MSDN, it's not explained at all.
Haacked.com has examples of using RouteLink in this manner:
routes.MapRoute(
name: "Test",
url: "code/p/{action}/{id}",
defaults: new { controller = "Section", action = "Index", id = "" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = "" }
);
#Html.RouteLink("Test", new {controller="section", action="Index", id=123})
#Html.RouteLink("Default", new {controller="Home", action="Index", id=123})
When I read that I thought it was a typo that both would actually bind to "Default" but no it actually works. The link with the text "Test" generates as https://localhost:44301/code/p/Index/123.
It seemed "wrong" because to me the default values are normally used when none are supplied but here they are being use as lookup values during generation. It's weird and loose and I can't remember ever having done it this way nor can I think of a reason why I would ever do it this way.
If I do this :
#Html.RouteLink("Test", new {controller="sectionA", action="Index", id=123})
it doesn't work but if I do this :
#Html.RouteLink("Test", new {controller="section", action="IndexA", id=123})
it still does so it seems only the controller value is significant and yet it's completely unused as far as the generated url goes.
https://localhost:44301/code/p/Index/123
At Haacked.com, Phil says:
There’s even more details I’ve glossed over having to do with how a
route’s default values figure into URL generation. That’s a topic for
another time, but it explains why you don’t run into this problem with
routes to controller actions which have an URL without parameters.
But it doesn't seem he ever got back to that topic. I've looked at the source but I'm 30 calls deep and wandering into abstract classes that lead nowhere and I'm not in a position to step through it off a symbol server right now.
So is it as simple as "controller names become route names, in the absence of actual route names during url generation"? And if so why did they do/ allow this?
Looks like that #Html.ActionLink() and #Html.RouteLink do the same thing if the route name is not passed.
Take a look at the following method (found in RouteCollectionExtensions.cs). name parameter is the route name here.
internal static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values, out bool usingAreas)
{
if (routes == null)
throw new ArgumentNullException("routes");
if (!string.IsNullOrEmpty(name))
{
usingAreas = false;
return routes.GetVirtualPath(requestContext, name, values);
}
string areaName = (string) null;
if (values != null)
{
object obj;
if (values.TryGetValue("area", out obj))
areaName = obj as string;
else if (requestContext != null)
areaName = AreaHelpers.GetAreaName(requestContext.RouteData);
}
RouteValueDictionary values1 = values;
RouteCollection routeCollection = RouteCollectionExtensions.FilterRouteCollectionByArea(routes, areaName, out usingAreas);
if (usingAreas)
{
values1 = new RouteValueDictionary((IDictionary<string, object>) values);
values1.Remove("area");
}
return routeCollection.GetVirtualPath(requestContext, values1);
}
You can see that it takes different actions based upon if the route name is null or not.
To answer your question, behavior for both the #Html.ActionLink() and #Html.RouteLink() will be the same (they will select the same route based upon configuration) if the route name is not passed.
Update
I am not sure how the controller name can become the route name because in the following method call you can see the nulls are being passed along for both the action name and controller name :
public static string GenerateRouteLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
{
return HtmlHelper.GenerateLinkInternal(requestContext, routeCollection, linkText, routeName, (string) null, (string) null, protocol, hostName, fragment, routeValues, htmlAttributes, false);
}
I've a hunt around and to paraphrase Professional ASP.NET MVC 4
By Jon Galloway, Phil Haack, Brad Wilson, K. Scott Allen in the scenario outlined in my question, default values not present in the url become a keyed switch of sorts that must be supplied with values that match the defaults in the RouteLink in order for that link to be considered a match for url generation.
So if you have a route like:
routes.MapRoute(
name: "Test",
url: "code/p/{action}/{id}",
defaults: new { controller = "Section", action = "Index", id = "" }
);
Then in order for it to be considered you must supply a controller="section" value in the routelink. {action} doesn't matter as it's part of the url and {controller} itself is not significant. It's any {parameter} with a default value not in the url.
So given
routes.MapRoute(
name: "Test",
url: "code/p/{action}/{id}",
defaults: new { fruit = "bananas", action = "Index", id = "" }
);
We'd need to supply a fruit="bananas" in my RouteLink
#Html.RouteLink("Test", new {fruit="bananas", action="IndexA", id=123})
Also note that it's not based on order. So it's not that the first {parameter} not present in the url becomes the key, it's all {parameter} not present in the url become the key so in effect you could have a composite key as per:
routes.MapRoute(
name: "Test",
url: "code/p/{action}/{id}",
defaults: new { fruit = "bananas", insect="honeybee", action = "Index", id = "" }
);
You'd need a RouteLink defined as below for a match:
#Html.RouteLink("Test", new {fruit="bananas", insect="honeybee", action="IndexA", id=123})
If you Google
a detailed look at url generation
You'll get the Google books link for the pages that explain this. I refer to this as a "keyed switch" because as per the flow chart a matching key turns route consideration on and off for a given route.
I still can't think of a good reason as to why I'd ever do this over a named route and thus why it's specifically supported.

Duplicate routes need to fallback to next matching route when no Action is found

I have a situation where I need to override methods in an old HttpHandler API in an ASP.NET WebForms application. I'm using WebAPI as the base for the new API and it's been working great except I need the routing to fallback when an action is not found in the new API. This is not true currently, instead I get a "404: Not Found" whenever the WebAPI controller does not have a matching Action.
My routing for the new API is as follows:
var apiroute = routes.MapHttpRoute(
"API Default",
"api/{language}-{country}/{controller}/{action}/{id}",
new { id = RouteParameter.Optional }
);
The old Httphandler APIs register their routes as follows (order is after WebAPI route):
var route = new Route("api/{language}-{country}/Person/{command}", this);
RouteTable.Routes.Add(route);
I want /api/en-US/Person/List to match the first route if theres a PersonController with a List action but to fallback to the old api if there is not.
Question: Is it possible to add a filter to the first route to only match if the Action is indeed available on the controller? How else could I accomplish this?
Update
You may also want to investigate attribute based routing this may be a cleaner solution only the actions you annotate with the routing attributes will be matched to the route and that may help you achieve what you want.
Or
Although there may be better ways than this, it is possible if you implement a custom IRouteConstraint e.g. this article.
A highly rough and ready approach you could improve upon is below:
public class IfActionExistsConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var actionName = values["Action"] as string;
if (actionName == null)
{
return false;
}
var controllerName = values["Controller"] as string;
var controllerTypeResolver = GlobalConfiguration.Configuration.Services.GetHttpControllerTypeResolver();
var controllerTypes = controllerTypeResolver.GetControllerTypes(GlobalConfiguration.Configuration.Services.GetAssembliesResolver());
var controllerType = controllerTypes.SingleOrDefault(ct => ct.Name == string.Format("{0}Controller", controllerName));
if(controllerType == null)
{
return false;
}
return controllerType.GetMethod(actionName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) != null;
}
}
Registration:
var apiroute = routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{language}-{country}/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { IfActionExistsConstraint = new IfActionExistsConstraint() }
);

How to have a route that goes to 2 destinations

I need the ability to have routes like the following URLs.
/authorid
/documentid
/authorid/documentid
I am trying to not to do
/author/1
/document/5
author/1/document/25
How can this be done?
Clarifications.
A document and an author can have the same id. BUT the author should take precedence.
Author: {ID: "djQuery" }
Document: { Aurhor: "Bob", ID: "djQuery" }
Should be accessible via /Bob/djQuery
But if you just go to /djQuery you should get a list of djQuery's documents.
You can achieve what you want with three routes and the appropriate RouteConstraints.
First, you need to establish your Routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Author_Document", // Route name
"{author}/{document}", // URL
new { controller = "ShowDocument", action = "AuthorDocument" }, // Parameters
new { author = new MustBeValidAuthor(), document = new MustBeValidDocument() }
routes.MapRoute(
"Author", // Route name
"{author}", // URL
new { controller = "ShowDocument", action = "Author" }, // Parameters
new { author = new MustBeValidAuthor() }
routes.MapRoute(
"Document", // Route name
"{document}", // URL
new { controller = "ShowDocument", action = "Document" }, // Parameters
new { document = new MustBeValidDocument() }
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
Next, you need to create your RouteConstraints, which must verify that the information provided is valid:
using System;
using System.Web;
using System.Web.Routing;
namespace ExampleApp.Extensions
{
public class MustBeValidAuthor : IRouteConstraint
{
public MustBeValidAuthor() { }
private DbContext _db = new DbContext();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return (_db.Authors.Where(u => u.AuthorName == values[parameterName].ToString()).Count() > 0);
}
}
public class MustBeValidDocument : IRouteConstraint
{
public MustBeValidDocument() { }
private DbContext _db = new DbContext();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return (_db.Documents.Where(u => u.DocumentName == values[parameterName].ToString()).Count() > 0);
}
}
}
The RouteConstraints will enfoce the validity of the request, and placing the Routes in this order fulfills your requirements.
I do not think that it is possible to do this just by configuring routes.
The problem is how does the program know if the number that you send is an authorid or a documentid.
What you can do is that /1 goes to a page, on that page you do a check to see if it is a document or an author, then you do a Response.Redirect to the actual page, say /author/1.
Assuming you want
/1
/2
/1/2
I don't think you can achive that beacuse the first two urls will call the same routing rule.
You can't know when to call a method instead of another.
The only solution I can think of, is that if you can discern the ids (for example id from 1 to 10 are authorid while the others are documentId) you can write your custom rulehandler and act accordinlgy calling the right method.
Otherwise you have to put something in the url to make it unique:
"/a1" for authorid and "/d2" for DoumentId would have something like that
routes.MapRoute("", "a{authorId}", new { controller = "Home", action = "AuthorDetail" }, new { authorId = #"\d+" });
routes.MapRoute("", "d{documentId}", new { controller = "Home", action = "DocumentDetail" }, new { documentId = #"\d+" });
EDIT: after your clarification the answer from counsellorben should do what you want
You can use something like this where IsAuthorConstraint has a look up to determine if document or author. Alternatively you can route to a bridging Action and put the logic there, before passing on to document or author action.
context.MapRoute(
"authorid",
"{authorid}",
new { action = "Author", controller = "Controller",
authorid = -1 },
new { authorid = new IsAuthorConstraint() }
);
context.MapRoute(
"document",
"{documentid}",
new { action = "Document", controller = "Controller",
documentid = -1 }
);
// assume overload on author action
context.MapRoute(
"document and author",
"{authorid}/{documentid}",
new { action = "Author", controller = "Controller",
documentid = -1, authorid = -1 }
);
I'm not at a computer so may not be exactly right, but hope you get the idea.
You can only achieve this with Route constraints as #counsellorben suggested but there should be no same id for authorid and documentid.
If there is, a conflict is going to occur there.

ASP.NET MVC catch-all routing

I have read a few threads on StackOverflow about this, but cannot get it to work. I have this at the end of my RegisterRoutes in Global.asax.
routes.MapRoute(
"Profile",
"{*url}",
new { controller = "Profile", action = "Index" }
);
Basically what I'm trying to achieve is to have mydomain.com/Username point to my member profilepage. How would I have to set up my controller and RegisterRoutes in order for this to work?
Currently mydomain.com/somethingthatisnotacontrollername gets a 404-error.
Solution that works for your case but is not recommended
You have a predefined set of controllers (usually less than 10) in your application so you can put a constraint on controller name and then route everything else to user profile:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "Home|Admin|Reports|..." }
);
routes.MapRoute(
"Profile",
"{username}/{action}",
new { controller = "Profile", action = "Details" }
);
But this will not work in case some username is the same as your controller name. It's a small possibility based on experience end empirical data, but it's not 0% chance. When username is the same as some controller it automatically means it will get handled by the first route because constraints won't fail it.
Recommended solution
The best way would be to rather have URL requests as:
www.mydomain.com/profile/username
Why do I recommend it to be this way? Becasue this will make it much simpler and cleaner and will allow to have several different profile pages:
details www.mydomain.com/profile/username
settings www.mydomain.com/profile/username/settings
messages www.mydomain.com/profile/username/messages
etc.
Route definition in this case would be like this:
routes.MapRoute(
"Profile",
"Profile/{username}/{action}",
new { controller = "Profile", action = "Details" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
having something that matches to mydomain.com/Username isn't really going to work because there is no way for the routing engine to distinguish between
mydomain.com/someusername
and
mydomain.com/controllername
What might be possible is, if your username scheme has a unique set of properties, i.e. is a sequence of 9 digits, you could define a route to check for something that looks like a username.
routes.MapRoute("",
"UserRoute",
"{username}",
new { controller = "Profile", action = "Index"},
new { {"username", #"\d{9}"}}
);
The key point tho, is that you need to provide some way for the routing engine to differentiate between a username and a standard controller action
You can find out more about constraints here
I had a requirement like this for my project. What i did was creating a route constraint like below:
public class SeoRouteConstraint : IRouteConstraint
{
public static HybridDictionary CacheRegex = new HybridDictionary();
private readonly string _matchPattern = String.Empty;
private readonly string _mustNotMatchPattern;
public SeoRouteConstraint(string matchPattern, string mustNotMatchPattern)
{
if (!string.IsNullOrEmpty(matchPattern))
{
_matchPattern = matchPattern.ToLower();
if (!CacheRegex.Contains(_matchPattern))
{
CacheRegex.Add(_matchPattern, new Regex(_matchPattern));
}
}
if (!string.IsNullOrEmpty(mustNotMatchPattern))
{
_mustNotMatchPattern = mustNotMatchPattern.ToLower();
if (!CacheRegex.Contains(_mustNotMatchPattern))
{
CacheRegex.Add(_mustNotMatchPattern, new Regex(_mustNotMatchPattern));
}
}
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var matchReg = string.IsNullOrEmpty(_matchPattern) ? null : (Regex)CacheRegex[_matchPattern];
var notMatchReg = string.IsNullOrEmpty(_mustNotMatchPattern) ? null : (Regex)CacheRegex[_mustNotMatchPattern];
var paramValue = values[parameterName].ToString().ToLower();
return IsMatch(matchReg, paramValue) && !IsMatch(notMatchReg, paramValue);
}
private static bool IsMatch(Regex reg, string str)
{
return reg == null || reg.IsMatch(str);
}
}
Then in the register route method:
routes.MapRoute("",
"UserRoute",
"{username}",
new { controller = "Profile", action = "Index"},
new { username = new SeoRouteConstraint(#"\d{9}", GetAllControllersName())}
);
The method GetAllControllersName will return all the controller name in your project separated by | :
private static string _controllerNames;
private static string GetAllControllersName()
{
if (string.IsNullOrEmpty(_controllerNames))
{
var controllerNames = Assembly.GetAssembly(typeof(BaseController)).GetTypes().Where(x => typeof(Controller).IsAssignableFrom(x)).Select(x => x.Name.Replace("Controller", ""));
_controllerNames = string.Join("|", controllerNames);
}
return _controllerNames;
}

Resources