48

Is there any easy/builtin way to make URL in lowercase for MVC3.

I have code that could do it but looking something simple to implement.

 routes.MapRouteLowercase(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "index", id = UrlParameter.Optional } // Parameter defaults
            );

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




public static class RouteCollectionExtensions
    {
        public static Route MapRouteLowercase(this RouteCollection routes, string name, string url)
        {
            return routes.MapRouteLowercase(name, url, null /* defaults */, (object)null /* constraints */);
        }

        public static Route MapRouteLowercase(this RouteCollection routes, string name, string url, object defaults)
        {
            return routes.MapRouteLowercase(name, url, defaults, (object)null /* constraints */);
        }

        public static Route MapRouteLowercase(this RouteCollection routes, string name, string url, object defaults, object constraints)
        {
            return routes.MapRouteLowercase(name, url, defaults, constraints, null /* namespaces */);
        }

        public static Route MapRouteLowercase(this RouteCollection routes, string name, string url, string[] namespaces)
        {
            return routes.MapRouteLowercase(name, url, null /* defaults */, null /* constraints */, namespaces);
        }

        public static Route MapRouteLowercase(this RouteCollection routes, string name, string url, object defaults, string[] namespaces)
        {
            return routes.MapRouteLowercase(name, url, defaults, null /* constraints */, namespaces);
        }

        public static Route MapRouteLowercase(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 = new RouteValueDictionary(defaults),
                Constraints = new RouteValueDictionary(constraints),
                DataTokens = new RouteValueDictionary()
            };

            if ((namespaces != null) && (namespaces.Length > 0))
            {
                route.DataTokens["Namespaces"] = namespaces;
            }

            routes.Add(name, route);

            return route;
        }

        public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url)
        {
            return context.MapRouteLowercase(name, url, (object)null /* defaults */);
        }

        public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url, object defaults)
        {
            return context.MapRouteLowercase(name, url, defaults, (object)null /* constraints */);
        }

        public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url, object defaults, object constraints)
        {
            return context.MapRouteLowercase(name, url, defaults, constraints, null /* namespaces */);
        }

        public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url, string[] namespaces)
        {
            return context.MapRouteLowercase(name, url, (object)null /* defaults */, namespaces);
        }

        public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url, object defaults, string[] namespaces)
        {
            return context.MapRouteLowercase(name, url, defaults, null /* constraints */, namespaces);
        }

        public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url, object defaults, object constraints, string[] namespaces)
        {
            if (namespaces == null && context.Namespaces != null)
            {
                namespaces = context.Namespaces.ToArray();
            }

            Route route = context.Routes.MapRouteLowercase(name, url, defaults, constraints, namespaces);
            route.DataTokens["area"] = context.AreaName;

            // disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
            // controllers belonging to other areas
            bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0);
            route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;

            return route;
        }
    }
Pirzada
  • 4,685
  • 18
  • 60
  • 113
  • Why do you want to do this? Apache, for example, is case-sensitive. If you have a case-insensitive webserver, why do you care about the case? – eaolson May 29 '11 at 15:15
  • He is asking because there is currently no easy way in MVC to make your action links display as lowercase (which looks good for consistency, and there is debate over whether or not it improves SEO). – Keith Jun 09 '11 at 21:35
  • 1
    @Keith is that because it reduces the idea of 2 URLs pointing to technically the same resource? How else does it improve SEO? – jamiebarrow Jun 17 '11 at 10:27
  • 3
    @jamiebarrow - Yes. By default /about and /About will render the same page, and if you happen to have links to both within your site, you could have "2 pages" with the exact same content - a no-no in SEO. – Keith Jun 17 '11 at 13:36
  • Thanks @Keith, I meant to say `reduces ranking based on the idea...` :) I guess there's also the other issue of `/about` and `/about/` pointing to the same content as well? Perhaps you should also think about that @pirzada – jamiebarrow Jun 17 '11 at 14:07

6 Answers6

99

I've just noticed that there is a new property in .NET Framework 4.5. Works great! RouteCollection.LowercaseUrls

Set LowercaseUrls to true

public static void RegisterRoutes(RouteCollection routes)
{
    routes.LowercaseUrls = true;
    ...
}

Create a link

@Html.ActionLink("Log in", "Login", "Account")

That will create awesome, lowercase url :)

<a href="/account/login">Log in</a>
krolik
  • 5,712
  • 1
  • 26
  • 30
17

There is a NuGet package for this : LowerCaseRoutesMVC (Project website)

Matthieu
  • 4,605
  • 4
  • 40
  • 60
Victor P
  • 1,496
  • 17
  • 29
12

If your reasons for enforcing lowercase is purely SEO, then the best solution that I have found is to use the IIS 7 URL Rewrite Module

Not only do you have the ability to force all url's to lowercase but you also have access to rules that allow you to remove/add trailing slashes, enforce canonical domains etc.

RuslanY's Blog has a good set to start from. For example, I use the following on all of my sites:

<!-- http://ruslany.net/2009/04/10-url-rewriting-tips-and-tricks/ -->
<rule name="Convert to lower case" stopProcessing="true">
    <match url=".*[A-Z].*" ignoreCase="false" />
    <conditions>
        <add input="{REQUEST_METHOD}" matchType="Pattern" pattern="GET" ignoreCase="false" />
    </conditions>
    <action type="Redirect" url="{ToLower:{R:0}}" redirectType="Permanent" />
</rule>

You simply add the above lines to the section of your web.config.

robasta
  • 4,621
  • 5
  • 35
  • 53
Jeremy Cade
  • 1,351
  • 2
  • 17
  • 28
  • It's important not to attempt to redirect POSTs because user agents won't re-POST after a redirect. Hence the condition matching GET. You might want to match HEAD too though - RFC2616 says "The action required MAY be carried out by the user agent without interaction with the user if and only if the method used in the second request is GET or HEAD." – Ben Challenor Oct 17 '12 at 09:12
  • Just an aside, installing URL Rewrite Module trashed T4MVC.tt for me – sydneyos Dec 28 '12 at 21:40
1

Yes, I have had to implement something similar to the above. It appears to be the only smooth way to accomplish this.

I would like to add that in addition, we added 301 redirects so that any traffic coming from /Upper-Case-Url will be 301 redirected to /upper-case-url.

Keith
  • 5,311
  • 3
  • 34
  • 50
1

Not sure what's wrong with your code (mine's a little different, but essentially the same concept). It's super easy to implement, and it's fully reusable.

LowerCaseRouteHelper.cs

using System.Web.Routing;
using System.Web.Mvc;

namespace Utilities.Helpers
{
    public class LowercaseRouteHelper : System.Web.Routing.Route
    {
        public LowercaseRouteHelper(string url, IRouteHandler routeHandler) : base(url, routeHandler)
        {
        }
        public LowercaseRouteHelper(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler)
        {
        }
        public LowercaseRouteHelper(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler)
        {
        }
        public LowercaseRouteHelper(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;
        }
    }

    public static class RouteCollectionExtensions
    {
        [System.Runtime.CompilerServices.Extension()]
        public static void MapRouteLowercase(RouteCollection routes, string name, string url, object defaults)
        {
            routes.MapRouteLowercase(name, url, defaults, null);
        }

        [System.Runtime.CompilerServices.Extension()]
        public static void MapRouteLowercase(RouteCollection routes, string name, string url, object defaults, object constraints)
        {
            if (routes == null) {
                throw new ArgumentNullException("routes");
            }

            if (url == null) {
                throw new ArgumentNullException("url");
            }

            object route = new LowercaseRouteHelper(url, new MvcRouteHandler()) {
                Defaults = new RouteValueDictionary(defaults),
                Constraints = new RouteValueDictionary(constraints)
            };

            if (String.IsNullOrEmpty(name)) {
                routes.Add(route);
            } else {
                routes.Add(name, route);
            }
        }
    }
}

global

routes.MapRouteLowercase("Start", "", new {
    controller = "Home",
    action = "Index"
})

I love this, and the great thing (as the comments say) is that it really helps improve SEO.

Chase Florell
  • 46,378
  • 57
  • 186
  • 376
  • Looks like @pirzada version is a bit extended version of that story http://goneale.com/2008/12/19/lowercase-route-urls-in-aspnet-mvc/. Also `Response.Status = "301 Moved Permanently";` is important. – angularrocks.com Aug 31 '11 at 14:04
0

Lol you guys are making this way too hard on yourselves. Just add this to your global.asax application_beginrequest ...

protected void Application_BeginRequest(Object sender, EventArgs e) {
string url = (Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Authority + HttpContext.Current.Request.Url.AbsolutePath);
if (Regex.IsMatch(url, @"[A-Z]")) {
  PermanentRedirect(url.ToLower() + HttpContext.Current.Request.Url.Query);
}
private void PermanentRedirect(string url) {
  Response.Clear();
  Response.Status = "301 Moved Permanently";
  Response.AddHeader("Location", url);
  Response.End();
}
guideX
  • 89
  • 3