Lowercase URLs in ASP.NET MVC (VB)

Sat, Feb 28, 2009 @ 11:46

There are a lot of acronyms in this title, but the bottom line is that I must say I am quite impressed with the changes that have been made to ASP.NET with the addition of their MVC extension. It takes away most of the awkwardness of ASP.NET’s web forms and leaves the rest of the quite robust application pipeline. However, in my time playing with it so far there was one issue I had.

A little background for my non-technical readers: ASP.NET is the web based application environment created by Microsoft which runs on Windows servers. Basically it is an alternative to writing web applications in PHP, Java, Ruby or any number of other popular options. Most of you know that my preference has generally been PHP. I’m not adverse to most of the alternatives, but that was the environment I started with oh so many years ago and it stayed with me.

The term MVC stands for Model, View and Controller. It is a concept in software engineering for the architecture of a program where you separate the presentational elements (view) from the business/domain logic (model). In between the two you have the interaction control (controller) which determines what model bits go with what view bits and generally just keeping everything in order. Generally this separation of concerns is considered to be a Good Thing™. Despite that, the lines are often blurred. There are many ways to maintain this separation, but systems (for the web anyway) which claim proper MVC status tend to go about it in similar ways. One popular system which uses this paradigm is Ruby on Rails. Traditionally ASP.NET did not really provide an MVC setup (I won’t go off on their traditional system now), so this is a pleasant departure. However, like I said: I had an issue.

I like my URLs to be lowercase. No exceptions. Some web servers, like those running on Windows, traditionally don’t distinguish between upper and lower case because Windows itself doesn’t. Unix-based systems traditionally do care. Except Macs. I say traditionally because I am talking about URLs which are served off of the filesystem. Now, there is no rule which states that the path portion of URLs must be lower case, in fact the W3C has a number of folders which are uppercase. Like I said the web server needs to handle it.

And now my issue, ASP.NET MVC by default will generate its URLs in links and forms and whatnot with the same case as the name and actions defined in the controller. Since the standard in .NET is to use Pascal case (ie. AccountController), this means that the URLs would contain /Account/ using the default generic route mapping. It is possible to define all of the routes specifically with lower case, but that defeats some of the purpose of the route matching.

The solution it turns out was already around on the Internet. The most acceptable solution I found in my quick search turned up a post by Luke Smith which took care of the generation of URLs by the system, and created redirects for URLs with uppercase letters. However, like most .NET code to be found online, it was written in C#. I needed it in Visual Basic. It wasn’t long, so I translated it. And then I added my own touch.

In total I created three files: LowercaseRoute.vb, EnforceLowercaseRequestHttpModule.vb, and RouteCollectionExtensionsLower.vb. At the moment, they are all sitting in my App_Code folder, but I will likely put these type of extensions into a class library at some point and include the assembly as they likely won’t be changing much. The LowercaseRoute class simply inherits from Route and overrides the GetVirtualPath method, forcing it to lowercase.

Public Class LowercaseRoute
    Inherits Route

    Public Sub New(ByVal url As String, ByVal routeHandler As IRouteHandler)
        MyBase.New(url, routeHandler)
    End Sub

    Public Sub New(ByVal url As String, ByVal defaults As RouteValueDictionary, ByVal routeHandler As IRouteHandler)
        MyBase.New(url, defaults, routeHandler)
    End Sub

    Public Sub New(ByVal url As String, ByVal defaults As RouteValueDictionary, ByVal contraints As RouteValueDictionary, ByVal routeHandler As IRouteHandler)
        MyBase.New(url, defaults, contraints, routeHandler)
    End Sub

    Public Overrides Function GetVirtualPath(ByVal requestContext As System.Web.Routing.RequestContext, ByVal values As System.Web.Routing.RouteValueDictionary) As System.Web.Routing.VirtualPathData
        Dim virtualPath As System.Web.Routing.VirtualPathData = MyBase.GetVirtualPath(requestContext, values)

        If virtualPath IsNot Nothing Then
            virtualPath.VirtualPath = virtualPath.VirtualPath.ToLowerInvariant()
        End If

        Return virtualPath
    End Function

End Class

The EnforceLowercaseRequestHttpModule class implements IHttpModule and redirects any URLs with uppercase letters. This one needs to be included in your web.config file. Add it under <system.webServer><modules> and change the type to include a namespace if necessary.

<add name="EnforceLowercaseRequestHttpModule" preCondition="" type="EnforceLowercaseRequestHttpModule"/>

EnforceLowercaseRequestHttpModule.vb:

Public Class EnforceLowercaseRequestHttpModule
    Implements IHttpModule


    Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
        AddHandler context.BeginRequest, AddressOf BeginRequest
    End Sub

    Private Sub BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        Dim app As HttpApplication = DirectCast(sender, HttpApplication)
        Dim requestedUrl As String = app.Context.Request.Url.Scheme & "://" & app.Context.Request.Url.Authority & app.Context.Request.Url.AbsolutePath

        If Regex.IsMatch(requestedUrl, "[A-Z]") Then
            Dim lowercaseUrl As String = requestedUrl.ToLowerInvariant() & HttpContext.Current.Request.Url.Query

            app.Context.Response.Clear()
            app.Context.Response.Status = "301 Moved Permanently"
            app.Context.Response.AddHeader("Location", lowercaseUrl)
            app.Context.Response.End()
        End If
    End Sub

    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose

    End Sub

End Class

And last but not least, I added an extension method to RouteCollection so I could keep the convenience of MapRoute only with my custom route class instead. I hope someone finds this useful. After this, you can call MapLowerRoute() to add all of your routes and let all of your URLs be lowercase. :)

Imports System.Runtime.CompilerServices

Module RouteCollectionExtensionsLower

    <Extension()> _
    Public Function MapLowerRoute(ByVal routes As RouteCollection, ByVal name As String, ByVal url As String, Optional ByVal defaults As Object = Nothing, Optional ByVal constraints As Object = Nothing, Optional ByVal namespaces As String() = Nothing) As Route
        If routes Is Nothing Then Throw New ArgumentException("routes")
        If url Is Nothing Then Throw New ArgumentException("url")

        Dim route As New LowercaseRoute(url, New RouteValueDictionary(defaults), New RouteValueDictionary(constraints), New MvcRouteHandler())

        If namespaces IsNot Nothing Then
            route.DataTokens = New RouteValueDictionary()
            route.DataTokens("Namespaces") = namespaces
        End If

        routes.Add(name, route)

        Return route
    End Function

End Module