I tend to write pretty long blog post titles.

There, I said it.

That's the first step, right? Admitting the problem? Except I don't usually think it's a problem... :) Nobody's going to type post URLs, anyway - long ones are really only an issue in one scenario: Twitter. And there are already plenty of solutions for URL shortening, so why does the world need another one?

It probably doesn't. But I wanted one anyway. In particular, I wanted something ridiculously simple to manage that I had complete control over and that didn't depend on how my web site host configured their server. I wanted to be sure I could use human-readable aliases and never need to worry about namespace collisions (a problem with existing services because all the good names have been taken). Besides, I don't see the point in relying on someone else (or some other web site) to do something I can do for myself pretty easily.

 

So as long as I'm writing my own URL shortener, I might as well give it a special power or something, right? Well, one thing that seemed pretty useful (to me and you) was an easy way to list all the supported aliases. Therefore, when you navigate to the root of the redirect namespace on my web site, that's just what you'll get:

Custom URL shortener in action

 

Now, when I want to point to a blog or sample of mine, instead of linking to it like this:

http://blogs.msdn.com/delay/archive/2009/07/19/my-new-home-page-enhanced-updated-collection-of-great-silverlight-wpf-data-visualization-resources.aspx

I can link to it like this:

http://cesso.org/r/DVLinks

What's more, because these redirects are of the 302/Found variety, I can update where they go when new content becomes available without having to change any existing content. This is something I've wanted for my links post(s) ever since I posted the first version! Problem solved: from now on, the DVLinks alias will always take you to my most recent post of Data Visualization links.

Some of the aliases above use MixedCase and some use ALLCAPS - there's actually meaning behind that. When I create an alias I intend to be widely useful and expect to use more than once, I'll use mixed case like I did with DVLinks. But when I'm creating an alias just so I can post it to Twitter, I'll use all caps like I did for TWITTER. This way, it's easy to browse my list of aliases and pick out the particularly interesting ones.

 

Here's the complete code for the IHttpModule-based URL shortener I wrote last night. It's standard ASP.NET and doesn't use any .NET 3.5 features, so it should work fine on pretty much any ASP.NET server.

public class UrlShortener : IHttpModule
{
    /// <summary>
    /// The prefix (directory/namespace) for all redirect requests.
    /// </summary>
    private const string RedirectPrefix = "~/r/";

    /// <summary>
    /// Maintains a mapping from alias to Uri for all redirect entries.
    /// </summary>
    private IDictionary<string, Uri> _aliases =
         new SortedDictionary<string, Uri>(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    /// Initializes the IHttpModule instance.
    /// </summary>
    /// <param name="context">HttpApplication instance.</param>
    public void Init(HttpApplication context)
    {
        // Populate the alias mappings
        AddAlias("ChartBuilder", "http://dlaa.me/Samples/ChartBuilder/");
        AddAlias("DVLinks",      "http://blogs.msdn.com/delay/archive/2009/07/19/my-new-home-page-enhanced-updated-collection-of-great-silverlight-wpf-data-visualization-resources.aspx");
        AddAlias("SLHash",       "http://dlaa.me/Samples/ComputeFileHashes/");
        AddAlias("TWITTER",      "http://blogs.msdn.com/delay/archive/2009/10/13/if-all-my-friends-jumped-off-a-bridge-apparently-i-would-too-just-created-a-twitter-account-davidans.aspx");

        context.BeginRequest += new EventHandler(HttpApplication_BeginRequest);
    }

    /// <summary>
    /// Adds an alias/Uri pair and validates it.
    /// </summary>
    /// <param name="alias">Alias to add.</param>
    /// <param name="uri">Uri to associate with the alias.</param>
    private void AddAlias(string alias, string uri)
    {
        // Validate the URI when adding it
        _aliases.Add(alias, new Uri(uri, UriKind.Absolute));
    }

    /// <summary>
    /// Handles the HttpApplication's BeginRequest event.
    /// </summary>
    /// <param name="source">HttpApplication source.</param>
    /// <param name="e">Event arguments.</param>
    private void HttpApplication_BeginRequest(object source, EventArgs e)
    {
        // Check if the request is a redirect attempt
        HttpApplication context = (HttpApplication)source;
        string requestPath = context.Request.AppRelativeCurrentExecutionFilePath;
        if (requestPath.StartsWith(RedirectPrefix, StringComparison.OrdinalIgnoreCase))
        {
            // Extract the alias
            string alias = requestPath.Substring(RedirectPrefix.Length);
            if (_aliases.ContainsKey(alias))
            {
                // Known alias; redirect the user there
                context.Response.Redirect(_aliases[alias].ToString());
            }
            else
            {
                // Invalid alias; write a simple list of all known aliases
                using (HtmlTextWriter writer = new HtmlTextWriter(context.Response.Output, ""))
                {
                    writer.RenderBeginTag(HtmlTextWriterTag.Html);
                    writer.RenderBeginTag(HtmlTextWriterTag.Head);
                    writer.RenderBeginTag(HtmlTextWriterTag.Title);
                    writer.Write("Supported Aliases");
                    writer.RenderEndTag();
                    writer.RenderEndTag();
                    writer.RenderBeginTag(HtmlTextWriterTag.Body);
                    foreach (string key in _aliases.Keys)
                    {
                        writer.AddAttribute(HtmlTextWriterAttribute.Href, _aliases[key].ToString());
                        writer.RenderBeginTag(HtmlTextWriterTag.A);
                        writer.Write(key);
                        writer.RenderEndTag();
                        writer.WriteBreak();
                        writer.WriteLine();
                    }
                    writer.RenderEndTag();
                    writer.RenderEndTag();
                }
                context.Response.End();
            }
        }
    }

    /// <summary>
    /// Disposes of resources.
    /// </summary>
    public void Dispose()
    {
    }
}

To enable this URL shortener for IIS 7, just save the code above to a .CS file, put that file in the App_Code directory at the root of your web site, and register it in web.config with something like the following:

<?xml version="1.0"?>
<configuration>
    <system.webServer>
        <modules>
            <add name="UrlShortener" type="UrlShortener"/>
        </modules>
    </system.webServer>
</configuration>

(For IIS 6, you'll need to change system.webServer to system.web and modules to httpModules - otherwise it's just the same.)