Advertisement
ASP.NET

Generating Traditional URLs with ASP.NET MVC3

by

There are certain truths in the world: we're born, we die, and URLs should end with a slash if it doesn't point to a file. The ASP.NET MVC framework bucks tradition and convention, and the built-in methods that generate URLs do so by omitting the trailing slash. It may seem like a non-issue (and to many people it's not one), but many developers, this author included, are bugged by them.


First, Some Background

URL stands for Uniform Resource Locator; it tells web-aware clients where to locate a particular resource on the Internet. The URL of http://www.example.com/directory/file.html points to a physical file (the resource) called file.html that resides in a directory called directory on a web server found at the example.com domain. When the web server for example.com receives a request for that URL, it knows exactly where to look for the resource. If it finds the file, it serves its contents; if not, it responds with an error.

Traditional web servers do the same thing for directory requests.

Consider the URL of http://www.example.com/directory/. This URL ends with a trailing slash, denoting a directory. When the web server receives this request, it looks for the directory directory, and if it finds it, it gets the default document and returns it to the client. Otherwise, it responds with an error.

These two examples demonstrate a simple process, but it can get more complex with ambiguous requests. For example, the URL http://www.example.com/ambiguous points to a resource called ambiguous, but it is unclear what that resource is. It could be a file, but there is no extension; it could be a directory, but there's no trailing slash. When receiving a request for this resource, a traditional web server goes through the following process:

  • The server looks for a file called ambiguous. If it finds one, it returns it. Otherwise...
  • It sends a response back to the client, redirecting it to http://www.example.com/ambiguous/
  • The client requests the new URL
  • The server looks for the ambiguous directory and returns the appropriate response

Without explicitly specifying a resource, the requester creates an overhead in both processing time and bandwidth usage.

Without explicitly specifying a resource, the requester creates an overhead in both processing time and bandwidth usage. For this reason, the prevailing rule of thumb has been to put the trailing slash on all URLs that point to a directory. Doing so completely eliminates the wasted processing time and bandwidth usage. It has become second nature to many (maybe most?) web veterans to always include the slash at the end of URLs that do not point to a file.

So where does ASP.NET MVC fit into this? Well, ASP.NET, as you probably know, typically runs on Microsoft's web server, called IIS. IIS, by default, behaves just like any other web server, but its true power lies in its ability to pass the handling of requests to ISAPI modules or, even better, .NET code. In the case of an ASP.NET MVC application, IIS passes each request to the MVC app for processing. There, the app determines if the request should be routed to a method on a controller, or if it should pass control back to IIS to find a physical file or directory on the file system. So the process of handling a request for http://www.example.com/ambiguous by IIS and an MVC app looks something like this:

Wait, wait, wait! If MVC applications ignore the trailing slash, what's the problem?

When the app routes the request to a controller, the method on that controller executes, processes whatever data it needs, and returns a result to the client. It does not redirect the client to another URL (unless the method is supposed to do that). MVC applications don't care if URLs end with a slash or not--in fact, MVC apps ignore trailing slashes. As long as the URL matches a pattern in the routing table, the MVC app will handle the request and return the requested response without causing any extra overhead.

Wait, wait, wait! If MVC applications ignore the trailing slash, what's the problem? Technically, there isn't one. The ambiguous URL actually points to a resource on the server: a method on a controller object in the application. But as stated earlier, web developers have been putting slashes at the end of their URLs years before Microsoft released the MVC framework. It's habit and convention to do so.

Microsoft's stance on the "issue" is simply to be consistent with the URLs used in our application, and that's a technically correct stance. But in typical Microsoft fashion, the MVC framework's helper methods only generate URLs without the trailing slash-meaning developers have to write their own code to achieve "correct" URLs. The solution presented in this article is two-fold:

  • Create an extension method that generates URLs
  • Write customized versions of the RouteLink() method.

Because this solution uses variations of RouteLink(), it's important that your routes are named. If you're not naming your routes, you should! Finally, some code!


Generating URLs

The MVC framework provides a class called UrlHelper. As its name implies, its purpose is to help with generating URLs for our application. It has a static method called GenerateUrl() that will do most of the hard work for us. All we have to do is provide a route name and route values. We'll create an extention method for HtmlHelper objects called RouteUrl(), and it will have two overloads.

Writing method overloads is pretty easy. There's typically one overload that performs all the work (one overload to rule them all), and the other overloads simply pass through their arguments to it. So you'll start by writing the main overload, and its code follows:

public static class HtmlHelperExtensions
{
    public static string RouteUrl(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues)
    {
        string url = UrlHelper.GenerateUrl(
            routeName, 
            null /*actionName*/, 
            null /*controllerName*/, 
            routeValues, 
            htmlHelper.RouteCollection, 
            htmlHelper.ViewContext.RequestContext, 
            true
        );

        return String.Format("{0}/", url);
    }
}

The extension method accepts three arguments. The first is the HtmlHelper object that this method operates on. Note that when calling this method, you do not need to pass the HtmlHelper object; that is done automatically for you. The second argument is the string containing the route's name. The GenerateUrl() method will use the route's name to return a URL properly formatted according to the route's pattern defined in the routing table. The last argument is a RouteValueDictionary object, which contains a set of key/value pairs with all the information needed to generate the URL for the route. This includes the controller and action names, as well as any parameters the UrlHelper may need to generate the URL for the specified route.

The first statement of the method calls UrlHelper.GenerateUrl() to generate the URL. There are two overloads for the GenerateUrl() method, and the above code calls the one with the least amount of parameters. The route name is passed, but you omit the parameters specifying action and controller names. These values are actually held within the routeValues object, which is passed to GenerateUrl() as fourth argument. The HtmlHelper object gives you the next two pieces of information you need, and the final argument passed tells GenerateUrl() to include the implicit MVC values of "action" and "controller".

After the URL is generated, the RouteUrl() method calls String.Format() to create a new string, essentially concatenating the URL and a trailing slash. Note that String.Format() isn't necessary, and you can simply write return url + "/";. How you create a string is up to you.

With the main worker overload written, now add the lazy one. Here is its code:

public static string RouteUrl(this HtmlHelper htmlHelper, string routeName, object routeValues)
{
    return RouteUrl(htmlHelper, routeName, new RouteValueDictionary(routeValues));
}

This code is straight forward. It calls the main RouteUrl() overload by passing in the appropriate data. Because extension methods are static methods, they can be called just like any other static method-which is the case in this code. Note that you could just as easily have written this code by calling the main RouteUrl() as an instance (extension) method, like this:

// Alternative version
public static string RouteUrl(this HtmlHelper htmlHelper, string routeName, object routeValues)
{
    return htmlHelper.RouteUrl(routeName, new RouteValueDictionary(routeValues));
}

It doesn't matter. Either way, the job gets done; it comes down to your personal preference.

Calling this method from your view is quite simple. First, make sure the namespace containing the HtmlHelperExtensions class is imported, and simply use the following syntax:

Html.RouteUrl("routeName", new 
    {
        controller = "ControllerName",
        action = "ActionName",
        parameterOne = "Hello",
        parameterTwo = "World"
    })

Of course, the values for controller and action, as well as the parameters, will be different for your specific MVC app. But this gives you an idea of how to use the method. Now that you can generate pretty URLs with a trailing slash, it's time to write more extension methods to generate your links.


Generating Links

One of the most helpful HtmlHelper methods is the RouteLink() method. All you have to do is provide it the route name and values to get a string containing an anchor element with a pretty relative URL to the specified action. Of course, RouteLink() returns anchor elements containing a URL without a trailing slash; so, you need to write your own method that uses RouteUrl().

There are eleven overloads for RouteLink(), so if you want to write your own version of all eleven, feel free. This article will only walk you through the creation of a few-likely the most popular overloads. The RouteLink() method is actually an extension method, and since extensions methods cannot be overridden or hidden, you'll have to come up with a name for your own method. This article gets super creative and uses MyRouteLink().

The main overload will accept five arguments: the HtmlHelper instance, a link's text, the route name, a RouteValueDictionary containing the route information, and a dictionary of HTML attributes to apply to the anchor element. Like RouteUrl(), MyRouteLink() is a static method of the HtmlHelperExtensions static class. Here is its code (to save space, the class declaration is omitted):

public static MvcHtmlString MyRouteLink(
    this HtmlHelper htmlHelper, 
    string linkText, 
    string routeName, 
    RouteValueDictionary routeValues, 
    IDictionary<string, object> htmlAttributes)
{
    string url = RouteUrl(htmlHelper, routeName, routeValues);

    TagBuilder tagBuilder = new TagBuilder("a")
    {
        InnerHtml = (!String.IsNullOrEmpty(linkText)) ? linkText : String.Empty
    };

    tagBuilder.MergeAttributes(htmlAttributes);
    tagBuilder.MergeAttribute("href", url);
        
    return MvcHtmlString.Create((tagBuilder.ToString(TagRenderMode.Normal)));
}

This code might look familiar to you if you have ever studied the ASP.NET MVC source code. The HtmlHelper class has a private method called GenerateRouteLink(), which was the inspiration for MyRouteLink(). Your method returns an MvcHtmlString object, which is an HTML-encoded string (you don't have to do any encoding yourself). The first statement of this method uses your RouteUrl() method to get your special URL. Next, you use a TagBuilder object to build an anchor element, populating its InnerHtml property with the link's text. Then the HTML attributes, those provided by the dictionary and the href attribute, are added to the element. Finally, the HTML output is created as a MvcHtmlString object and returned to the caller.

It's cake from now on, as the remaining overloads will, in one way or another, call this version of MyRouteLink(). The next overload has a similar signature; the route values and attributes will simply be objects. Here is its code:

public static MvcHtmlString MyRouteLink(
    this HtmlHelper htmlHelper, 
    string linkText, 
    string routeName, 
    object routeValues, 
    object htmlAttributes)
{
    return MyRouteLink(
        htmlHelper,
        linkText,
        routeName,
        new RouteValueDictionary(routeValues),
        new RouteValueDictionary(htmlAttributes)
    );
}

This code is self-explanitory. You call the main MyRouteLink() overload by passing in the appropriate data. Note you're using a RouteValueDictionary object for the HTML attributes; it's a suitable container for HTML attributes.

The next, and final, two overloads are more of the same, except they do not accept an argument for HTML attributes. Here is their code:

public static MvcHtmlString MyRouteLink(
    this HtmlHelper htmlHelper, 
    string linkText, 
    string routeName, 
    RouteValueDictionary routeValues)
{
    return MyRouteLink(
        htmlHelper,
        linkText,
        routeName, 
        routeValues,
        new RouteValueDictionary()
    );
}

public static MvcHtmlString MyRouteLink(
    this HtmlHelper htmlHelper,
    string linkText,
    string routeName,
    object routeValues)
{
    return MyRouteLink(
        htmlHelper,
        linkText,
        routeName,
        new RouteValueDictionary(routeValues)
    );
}

The first overload in the above code omits the HTML attribute collection and specifies a RouteValueDictionary object for the route values. It calls the overload with a signature of MyRouteLink(HtmlHelper, string, string, RouteValueDictionary, IDictionary<string, object>), and passes an empty RouteValueDictionary object for the HTML attributes. The second overload has a slightly different signature, accepting an object for the route values. It calls the first overload in this code listing to generate the link.

To use this helper method, be sure you import the namespace containing the HtmlHelperExtensions class in your view. Then, you can write something like this:

@Html.MyRouteLink("My Link Text", "route name", new 
    {
        controller = "ControllerName",
        action = "ActionName",
        parameterOne = "Hello",
        parameterTwo = "World"
    }
);

This code uses Razor syntax, but you can essentially do the same thing if using the ASPX view engine, like this:

<%:Html.MyRouteLink("My Link Text", "route name", new 
    {
        controller = "ControllerName",
        action = "ActionName",
        parameterOne = "Hello",
        parameterTwo = "World"
    }
) %>

This code uses the new <%: %> nugget for HTML encoding output (introduced in .NET 4). The beauty of the MvcHtmlString returned by MyRouteLink() is that it's already HTML encoded, and it will not be re-encoded by using the new nugget. So you can use <%: %> or <%= %> without worrying about encoding or re-encoding.


In Closing

As mentioned earlier, there is nothing technically wrong with the URLs genereated by the MVC framework. They do not cause an overhead, as they point to an actual resource on the server. But if you've been bugged by them, now you can generate traditionally correct URLs with just a little work upfront. Microsoft is right, however: be consistent. Regardless of what URL style you prefer, make sure you consistently use the same one.

Let me know if you have any questions in the comments and thank you so much for reading!

Related Posts
  • Code
    ASP.NET
    Preventing Code InjectionCsrf dotnet retina preview
    Often, websites seem to exist primarily to put something into a database in order to pull it out later. While other database methods, such as NoSQL, have gained popularity in recent years, data for many websites still resides in the traditional SQL database. This data often consists of valuable personal information such as credit card numbers and other personal information of interest to identity thieves and criminals. Hackers therefore always look to get this data. One of the most common targets of these attacks is the SQL databases that lie behind many web applications through a process of SQL Injection.Read More…
  • Code
    PHP
    Building a Customer Management App Using AngularJS and LaravelLaravel 4 auth retina preview
    When creating a single-page app we should use some kind of framework to do some of the job for us, so we can focus on the actual functionality. AngularJS fits here perfectly, because features like dynamic dependency injection and bi-directional data binding are just great. Sometimes we also require some kind of server. If you've chosen PHP then Laravel may be your best option, as it's easy to work with and pretty powerful.Read More…
  • Code
    JavaScript & AJAX
    Introduction to Sails.jsSails preview 400 2
    Sails is a Javascript framework designed to resemble the MVC architecture from frameworks like Ruby on Rails. It makes the process of building Node.js apps easier, especially APIs, single page apps and realtime features, like chat.Read More…
  • Code
    Scala
    Building Ribbit in ScalaRibbit scala retina preview
    In this tutorial we will implement the Ribbit application in Scala. We'll be covering how to install the Play web framework, a NetBeans plugin for it, and finally the code in Scala. If you are new to Scala, check out this previous tutorial which will help you set up your environment and provides you with a general platform that you can build upon. Even though the essence of Ribbit is to create/send/read Ribbits (our version of tweets), we will spend a large part of this tutorial explaining how Play works, authentication, and persistence. After these are in place, the rest becomes much easier. We will also implement ribbit creation, submission and listing out all ribbits. Following someone, advanced user settings, and direct messages will be an extra assignment for you to complete on your own. I am sure if you manage to follow along with this tutorial and create Ribbit as explained below, these three functionalities will be easily accomplished as homework.Read More…
  • Code
    Articles
    Web Assets - Tips for Better Organization and PerformanceWeb assets retina preview
    Remember back to when we had to spend a lot of time optimizing our project's assets (images, CSS, etc..)? Well today, users have a much faster Internet connection and it appears that we can afford to use bigger images or bigger flash files with a lot of video and pictures inside. However, with the rise of mobile development, we are again back in that same situation. It is extremely important to create well optimized sites, so that we have faster applications, which download less content and respond immediately. Read More…
  • Code
    JavaScript & AJAX
    Introduction to ExpressExpress retina preview
    A few years ago I, like many people, began to hear more and more about Node.js. I had been building server-side applications in ColdFusion for over a decade but was always curious to see how other platforms worked as well. I enjoyed JavaScript on the client and using it on the server seemed fascinating. (Although not necessarily new. I'm old enough to remember when Netscape released SSJS back in the 90s.) I read a few tutorials, sat in a few sessions, and in general came away... unimpressed. Read More…