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: 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