How can I have lowercase routes in ASP.NET MVC? - asp.net-mvc

How can I have lowercase, plus underscore if possible, routes in ASP.NET MVC? So that I would have /dinners/details/2 call DinnersController.Details(2) and, if possible, /dinners/more_details/2 call DinnersController.MoreDetails(2)?
All this while still using patterns like {controller}/{action}/{id}.

With System.Web.Routing 4.5 you may implement this straightforward by setting LowercaseUrls property of RouteCollection:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Also assuming you are doing this for SEO reasons you want to redirect incoming urls to lowercase (as said in many of the links off this article).
protected void Application_BeginRequest(object sender, EventArgs e)
{
//You don't want to redirect on posts, or images/css/js
bool isGet = HttpContext.Current.Request.RequestType.ToLowerInvariant().Contains("get");
if (isGet && !HttpContext.Current.Request.Url.AbsolutePath.Contains("."))
{
//You don't want to modify URL encoded chars (ex.: %C3%8D that's code to Í accented I) to lowercase, than you need do decode the URL
string urlLowercase = Request.Url.Scheme + "://" + HttpUtility.UrlDecode(HttpContext.Current.Request.Url.Authority + HttpContext.Current.Request.Url.AbsolutePath);
//You want to consider accented chars in uppercase check
if (Regex.IsMatch(urlLowercase, #"[A-Z]") || Regex.IsMatch(urlLowercase, #"[ÀÈÌÒÙÁÉÍÓÚÂÊÎÔÛÃÕÄËÏÖÜÝÑ]"))
{
//You don't want to change casing on query strings
urlLowercase = urlLowercase.ToLower() + HttpContext.Current.Request.Url.Query;
Response.Clear();
Response.Status = "301 Moved Permanently";
Response.AddHeader("Location", urlLowercase);
Response.End();
}
}
}

These two tutorials helped when I wanted to do the same thing and work well:
http://www.coderjournal.com/2008/03/force-mvc-route-url-lowercase/
http://goneale.com/2008/12/19/lowercase-route-urls-in-aspnet-mvc/
EDIT: For projects with areas, you need to modify the GetVirtualPath() method:
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var lowerCaseValues = new RouteValueDictionary();
foreach (var v in values)
{
switch (v.Key.ToUpperInvariant())
{
case "ACTION":
case "AREA":
case "CONTROLLER":
lowerCaseValues.Add(v.Key, ((string)v.Value).ToLowerInvariant());
break;
default:
lowerCaseValues.Add(v.Key.ToLowerInvariant(), v.Value);
break;
}
}
return base.GetVirtualPath(requestContext, lowerCaseValues);
}

If you happened to use ASP.NET Core, you probably should have a look at this:
Add the following line to the ConfigureServices method of the Startup class.
services.AddRouting(options => options.LowercaseUrls = true);

If you are using the UrlHelper to generate the link, you can simply specify the name of the action and controller as lowercase:
itemDelete.NavigateUrl = Url.Action("delete", "photos", new { key = item.Key });
Results in: /media/photos/delete/64 (even though my controller and action are pascal case).

I found this at Nick Berardi’s Coder Journal, but it did not have information on how to implement the LowercaseRoute class. Hence reposting here with additional information.
First extend the Route class to LowercaseRoute
public class LowercaseRoute : Route
{
public LowercaseRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler) { }
public LowercaseRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler) { }
public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
: base(url, defaults, constraints, routeHandler) { }
public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler) { }
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData path = base.GetVirtualPath(requestContext, values);
if (path != null)
path.VirtualPath = path.VirtualPath.ToLowerInvariant();
return path;
}
}
Then modify the RegisterRoutes method of Global.asax.cs
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LowercaseRoute("{controller}/{action}/{id}",
new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
new MvcRouteHandler()));
//routes.MapRoute(
// "Default", // Route name
// "{controller}/{action}/{id}", // URL with parameters
// new { controller = "Home", action = "Index", id = "" } // Parameter defaults
//);
}
I would however like to know a way to use routes.MapRoute...

You can continue use the MapRoute syntax by adding this class as an extension to RouteCollection:
public static class RouteCollectionExtension
{
public static Route MapRouteLowerCase(this RouteCollection routes, string name, string url, object defaults)
{
return routes.MapRouteLowerCase(name, url, defaults, null);
}
public static Route MapRouteLowerCase(this RouteCollection routes, string name, string url, object defaults, object constraints)
{
Route route = new LowercaseRoute(url, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints)
};
routes.Add(name, route);
return route;
}
}
Now you can use in your application's startup "MapRouteLowerCase" instead of "MapRoute":
public void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Url shortcuts
routes.MapRouteLowerCase("Home", "", new { controller = "Home", action = "Index" });
routes.MapRouteLowerCase("Login", "login", new { controller = "Account", action = "Login" });
routes.MapRouteLowerCase("Logout", "logout", new { controller = "Account", action = "Logout" });
routes.MapRouteLowerCase(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}

This actually has two answers:
You can already do this: the route engine does case-insensitive comparison. If you type a lower-case route, it will be routed to the appropriate controller and action.
If you are using controls that generate route links (ActionLink, RouteLink, etc.) they will produce mixed-case links unless you override this default behavior.
You're on your own for the underscores, though...

Could you use the ActionName attribute?
[ActionName("more_details")]
public ActionResult MoreDetails(int? page)
{
}
I don't think case matters. More_Details, more_DETAILS, mOrE_DeTaILs in the URL all take you to the same Controller Action.

Related

MVC Custom Routing Subdomain

I'm trying to build a "Tenant" Subdomain route that attaches to a MVC Area. In this case I have an Area called "Tenant" which has two controllers; Public and Admin. My custom Route is used to grab the Subdomain if it matches then route them to the proper Controller-Action-Area.
The base of this project came from the following
http://www.matrichard.com/post/asp.net-mvc-5-routing-with-subdomain
The problem I'm having is in the custom Subdomain Route. When I hit the Public/Index Route, the routeData is returning null and I see the following error. Although if the route is /admin it returns the correct routeData.
Server Error in '/' Application.
The matched route does not include a 'controller' route value, which is required.
It also seems to be always matching using RouteDebugger tool, is this a clue to my problem?
Examples Routes:
controller=Public action=Index, area=Tenant
http://tenant1.mydomain.com:8080/
http://tenant1.mydomain.com:8080/logon
controller=Admin action=Index, area=Tenant
http://tenant1.mydomain.com:8080/admin
http://tenant1.mydomain.com:8080/admin/edit
--
SubdomainRouteP.cs
public class SubdomainRouteP : Route
{
public string Domain { get; set; }
public SubdomainRouteP(string domain, string url, RouteValueDictionary defaults): this(domain, url, defaults, new MvcRouteHandler())
{
}
public SubdomainRouteP(string domain, string url, object defaults): this(domain, url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
}
public SubdomainRouteP(string domain, string url, object defaults, IRouteHandler routeHandler): this(domain, url, new RouteValueDictionary(defaults), routeHandler)
{
}
public SubdomainRouteP(string domain, string url, RouteValueDictionary defaults, IRouteHandler routeHandler): base(url, defaults, routeHandler)
{
this.Domain = domain;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//
// routeData object returns null in some cases
//
var routeData = base.GetRouteData(httpContext);
var subdomain = httpContext.Request.Url.Host.Split('.').First();
string[] blacklist = { "www", "mydomain", "localhost" };
// This will ignore anything that is not a client tenant prefix
if (blacklist.Contains(subdomain))
{
return null; // Continue to the next route
}
// Why is this NULL?
if (routeData == null)
{
routeData = new RouteData(this, new MvcRouteHandler());
}
routeData.DataTokens["Area"] = "Tenant";
routeData.DataTokens["UseNamespaceFallback"] = bool.FalseString;
routeData.Values.Add("subdomain", subdomain);
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return routeData;
}
}
RouteConfig.cs
routes.Add("Admin_Subdomain", new SubdomainRouteP(
"{client}.mydomain.com", //of course this should represent the real intent…like I said throwaway demo project in local IIS
"admin/{action}/{id}",
new { controller = "Admin", action = "Index", id = UrlParameter.Optional }));
routes.Add("Public_Subdomain", new SubdomainRouteP(
"{client}.mydomain.com", //of course this should represent the real intent…like I said throwaway demo project in local IIS
"{controller}/{action}/{id}",
new { controller = "Public", action = "Index", id = UrlParameter.Optional }));
// This is the MVC default Route
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
The Url below gives me the following results from RouteDebugger. During test 1 and 2 the route still matches /admin.
Failed Test 1: http://tenant.mydomain.com/
Failed Test 2: http://tenant.mydomain.com/logon
Successful 3: http://tenant.mydomain.com/admin
Matches Url Defaults
True admin/{action}/{id} controller = Admin, action = Index
True {controller}/{action}/{id} controller = Public, action = Index
The post that you linked to has a bug: When a constraint or the URL does not match, the base.GetRouteData method will return null. In this case, adding the subdomain name to the route dictionary will obviously throw an exception. There should be a null guard clause before that line.
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData != null)
{
routeData.Values.Add("client", httpContext.Request.Url.Host.Split('.').First());
}
return routeData;
}
As should be the case with your route. You need to ensure you return null in the case where the base class returns null (which indicates either the URL or a constraint did not match, and we need to skip processing this route).
Also, I am not sure if it makes any difference than adding the data directly to the DataTokens, but the MVC framework has an IRouteWithArea that can be implemented to configure the Area the route applies to.
public class SubdomainRouteP : Route, IRouteWithArea
{
public string Area { get; private set; }
public SubdomainRouteP(string area, string url, RouteValueDictionary defaults): this(area, url, defaults, new MvcRouteHandler())
{
}
public SubdomainRouteP(string area, string url, object defaults): this(area, url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
}
public SubdomainRouteP(string area, string url, object defaults, IRouteHandler routeHandler): this(area, url, new RouteValueDictionary(defaults), routeHandler)
{
}
public SubdomainRouteP(string area, string url, RouteValueDictionary defaults, IRouteHandler routeHandler): base(url, defaults, routeHandler)
{
this.Area = area;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
// This will ignore anything where the URL or a constraint doesn't match
// in the call to base.GetRouteData().
if (routeData != null)
{
var subdomain = httpContext.Request.Url.Host.Split('.').First();
string[] blacklist = { "www", "mydomain", "localhost" };
// This will ignore anything that is not a client tenant prefix
if (blacklist.Contains(subdomain))
{
return null; // Continue to the next route
}
routeData.DataTokens["UseNamespaceFallback"] = bool.FalseString;
routeData.Values.Add("subdomain", subdomain);
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return routeData;
}
}
I can't figure out what you are trying to do with the domain parameter. The URL will most likely return something for domain. So, it seems like you should have a constraint in the first "{controller}/{action}/{id}" route or you will never have a case that will pass through to the default route. Or, you could use an explicit segment in the URL so you can differentiate it (the same way you did with your admin route).
routes.Add("Admin_Subdomain", new SubdomainRouteP(
"Tenant",
"admin/{action}/{id}",
new { controller = "Admin", action = "Index", id = UrlParameter.Optional }));
routes.Add("Public_Subdomain", new SubdomainRouteP(
"Tenant",
"public/{action}/{id}",
new { controller = "Public", action = "Index", id = UrlParameter.Optional }));
// This is the MVC default Route
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
Another option would be to add another constructor parameter to pass in an explicit list of valid domains to check against.

How to get the TLD with MapRoute?

I have many host names on IIS that point to the same ASP.NET MVC app.
www.domain.com
www.domain.co.uk
www.domain.net
...
How can I get the top level domain (es. ".com") when I set a map route like the following?
Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
' MapRoute takes the following parameters, in order:
' (1) Route name
' (2) URL with parameters
' (3) Parameter defaults
routes.MapRoute( _
"Default", _
"{controller}/{action}/{id}", _
New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional} _
)
End Sub
You need to create a custom route. Just inherit Route, override GetRouteData and include routing information which is for the TLD.
http://msdn.microsoft.com/en-us/library/system.web.routing.route.getroutedata.aspx
Example
The route:
public class RouteWithTld : Route
{
public RouteWithTld(string url, IRouteHandler routeHandler) : base(url, routeHandler)
{
}
public RouteWithTld(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler)
{
}
public RouteWithTld(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler)
{
}
public RouteWithTld(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var baseData = base.GetRouteData(httpContext);
int pos = httpContext.Request.Url.Host.LastIndexOf('.');
string tld;
if (pos == -1)
{
if (httpContext.Request.Url.Host == "localhost")
tld = "com";
else
{
throw new InvalidOperationException("You need to handle this case.");
}
}
else
tld = httpContext.Request.Url.Host.Substring(pos + 1);
baseData.Values.Add("tld", tld);
return baseData;
}
}
Mapping it in global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
/*routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
* */
var defaults =
new RouteValueDictionary(new {controller = "Home", action = "Index", id = UrlParameter.Optional});
routes.Add(new RouteWithTld("{controller}/{action}/{id}", defaults, new MvcRouteHandler()));
}
And accessing the parameter in the controller:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC. TLD: " + RouteData.Values["tld"];
return View();
}
public ActionResult About()
{
return View();
}
}

With MVC3, how can I change the controller/action based on the "accept" header?

I have an application that is going to act as a "catch-all" for requests that could be coming from a variety of targets. I would like to be able to redirect to a different controller/action in my application based on the value of the "accept" header.
Clarification: I would like to do this without an HTTP Handler, if possible.
You could write a custom route:
public class MyRoute : Route
{
public MyRoute(string url, object defaults)
: base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var accept = httpContext.Request.Headers["Accept"];
if (string.Equals("xml", accept, StringComparison.OrdinalIgnoreCase))
{
rd.Values["action"] = "xml";
}
else if (string.Equals("json", accept, StringComparison.OrdinalIgnoreCase))
{
rd.Values["action"] = "json";
}
return rd;
}
}
and then register this route:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(
"Default",
new MyRoute(
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
)
);
}
Now when you POST to /home and set the Accept request header to xml the Xml action of the Home controller will be hit.
make a route.. just a simple class and derive it from RouteBase here you will find the method GetRouteData(System.Web.HttpContextBase httpContext) with return type of RouteData
u can pick out the headers of your choice from the httpcontext and add the values of ur route to the return value of the function..
you can use Phil haack Route Magic plugin it has HttpHandler Routing but it use HttpHandler you can take a look , see if you like it
Route Magic

allow hyphens in URLs for asp.net mvc 2 controller names

[ActionName("about-us")]
public ActionResult EditDetails(int id)
{
// your code
}
The above works for actions but I would like to be able to do the same (or similar) for controllers, ie have a hyphen in the URL name too. Is there any easy way to achieve this (I tried the ActionName attribute but no luck)
Easiest way would be adding a custom route:
routes.MapRoute("RouteName", "controler-name/{action}/{id}", new { controller = "ControllerName", action = "Index", id = "" });
I haven't seen a controller name attribute like that before although it may be possible.
You can use custom route handler to give you needed functionality:
public class HyphenatedRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
And the route should be registered using that handler:
var route = routes.MapRoute(
"Some Action",
"{controller}/{action}/{id}"
);
route.RouteHandler = new HyphenatedRouteHandler();
There is a similar quastion asked here: ASP.net MVC support for URL's with hyphens
Hyphenated route in the route table should be before the default route.
routes.MapRoute(
"InformationAbout",
"information-about/{action}/{id}",
new { controller = "InformationAbout", action = "Index", id = "" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
Maybe this works:
public class CustomControllerFactory : DefaultControllerFactory {
protected override Type GetControllerType(RequestContext requestContext, string controllerName) {
return base.GetControllerType(requestContext, controllerName.Replace("-", ""));
}
}
May be here is the correct answer to the question. All other are workarounds which work for a single url but this one is a generic approach
http://blog.didsburydesign.com/2010/02/how-to-allow-hyphens-in-urls-using-asp-net-mvc-2/

Best way to format a query string in an asp.net mvc url?

I've noticed that if you sent a query string routevalue through asp.net mvc you end up with all whitespaces urlencoded into "%20". What is the best way of overriding this formatting as I would like whitespace to be converted into the "+" sign?
I was thinking of perhaps using a custom Route object or a class that derives from IRouteHandler but would appreciate any advice you might have.
You could try writing a custom Route:
public class CustomRoute : Route
{
public CustomRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler)
{ }
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var path = base.GetVirtualPath(requestContext, values);
if (path != null)
{
path.VirtualPath = path.VirtualPath.Replace("%20", "+");
}
return path;
}
}
And register it like this:
routes.Add(
new CustomRoute(
"{controller}/{action}/{id}",
new RouteValueDictionary(new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}),
new MvcRouteHandler()
)
);

Resources