A URL Resolver Module for ASP.NET MVC

Update: An improved version of this module, along with some performance stats, is available here. The original version posted here was very, very slow. Probably not a good idea to use it for anything. Ever.

One of the few things I actually liked about ASP.NET WebForms was that you could do things like

<a href=”~/my/account.aspx” runat=”server”>My Account</a>

and ASP.NET would magically turn the tilde character (~) into the current relative application root – so you could debug your apps on http://localhost:4567/ and then deploy them to http://www.myserver.com/some/app/, and your links wouldn’t break.

ASP.NET MVC doesn’t like things that are runat=”server” – and with good reason, I think – but this does mean you can end up with rather a lot of calls to ResolveUrl() sprinked throughout your code.

To get around this, I’ve hacked together an HTTP module that basically rewrites the output stream on the fly. It wraps the HTTP output stream (the thing you're writing to when you Response.Write stuff) in a 'smart' stream wrapper, and the magic naively optimistic part looks like this:

public override void Write(byte[] buffer, int offset, int count) {
  if (HttpContext.Current.Handler is System.Web.Mvc.MvcHandler) {
    HttpContext.Current.Trace.Warn("Resolving URLs in output stream...");
    byte[] data = new byte[count];
    Buffer.BlockCopy(buffer, offset, data, 0, count);
    string html = Encoding.ASCII.GetString(data);

    // Don't try and use Regex transformations on your 
    // entire output stream. It is slow. Like, really, really slow.
    // Take a look at this updated version instead.

    var re = new Regex("(?src|href|action)=\"~/", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
    html = re.Replace(html, "${attr}=\"" + VirtualPathUtility.ToAbsolute("~/"));
    data = Encoding.ASCII.GetBytes(html);
    sink.Write(data, 0, html.Length);
    HttpContext.Current.Trace.Warn("Resolved URLs in output stream.");
  } else {
    sink.Write(buffer, offset, count);

Basically, it looks for HTML SRC, ACTION and HREF attributes whose value begins with ~/, and replaces the ~ with the application’s virtual path on the fly. I haven’t tested this code for performance, so I don’t know what kind of impact it’ll have on your page response times, This code is something like 200 times slower than a straight stream copy, but it’s running in a couple of demo apps I’m working on and it seems to work pretty nicely.

The full implementation is over on Google Code if you’re interested.