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 thedirectory
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!
"
No comments:
Post a Comment