Lowercase URLs in ASP.NET MVC (VB)
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:
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
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"/>
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