Thursday, January 3, 2008

Application Root is your friend

It still surprises me how many ASP.NET developers I run into don't know about the different ways to construct path references in ASP.NET.  Let's say we want to include an image in our website.  This image is hosted on our website, in an "img" subfolder off of the application root.  So how do we create the image HTML, and what do we use as the URL?  The wrong answer can lead to big-time maintenance headaches later.

There are three kinds of paths we can use:

  • Absolute
  • Relative
  • Application root (ASP.NET only)

Additionally, we have a few choices on how we chose to create the image in our ASP.NET page:

  • Plain ol' HTML
  • HTML server control
  • Web server control

Each kind of path can be used for each rendering object type (HTML, server control).  It turns out that the path is much more important than the rendering object, as different forces might lend me to use controls over HTML.  For posterity, I'll just pick plain ol' HTML as an example.

Absolute

Absolute paths are fully qualified URL paths that include the domain name in the URL:

<img src="http://localhost/EcommApp/img/blarg.jpg" />

Absolute paths work great for external resources outside of my website, but are poor choices for internal resources.  Typically ASP.NET development is done on a development machine, and deployed to a different machine, which means the URLs will most likely change.

For example, the URL above works on my local machine, but breaks when deployed to the server because the "EcommApp" now resides at the root, so I need a URL like "http://ecommapp.com/img/blarg.jpg".  Since this absolute path is different, my link breaks, and I have to make lots of changes going back and forth between production and development.  For internal resources, absolute paths won't work.

Relative

Relative paths don't specify the domain name, and come in a few flavors:

  • Site-root relative
  • Current page relative
  • Peer relative

These URL path notations are similar to file path notations.  Each is slightly different and carries its own issues.

Site-root relative

Here's the same img tag used before, now with a site-root relative path:

<img src="/EcommApp/img/blarg.jpg" />

Note the lack of the domain name and the leading slash, that's what makes this a site-root relative path.  These paths are resolved against the current domain, which means I can go from "localhost" to "ecommapp.com" with ease.

Again, the problem I run into is that locally, my app is deployed off of an "EcommApp" folder, but on the server, it's deployed at the root.  My image breaks again, so site-root relative paths aren't a great choice, either.

Current page relative

Now the img tag using a current page relative path:

<img src="img/blarg.jpg" />

This time, I don't have the leading slash, nor do I include the "EcommApp" folder.  This is because current page relative paths are constructed off the URL being requested, which in this case is the "default.aspx" page at the root of the application.  The request goes from the "default.aspx" path, wherever that might be.  Now my URL does not have to change when I deploy to production, it works in both places.

But I have two problems now:

  • Moving the page means I have to change all of the resource URLs
  • Creating a page in a subfolder means all URLs to the same resource could be different

This leads me to the last kind of relative path.

Peer relative

Suppose I want to create the img tag in a site with the following structure:

  • \Root
    • \img
      • blarg.jpg
    • \products
      • default.aspx

Note that default.aspx has to go up one node, then down one node to reference the file in the above tree.  Here's the img tag to do just that:

<img src="../img/blarg.jpg" />

Similar to folder paths, I use the ".." operator to climb up one node in the path, then specify the rest of the path.  This path works just fine in production and development, but I still have two main problems:

  • URLs to the same resource are different depending on depth of the source file in the tree
  • Moving a resource forces me to manually fix the relative paths

If I decide to move the "default.aspx" page up one level, all of the relative paths must be manually fixed.

But there's one more major issue.

User controls

Now let's suppose I have the following setup:

  • \Root
    • \img
      • blarg.jpg
    • \products
      • default.aspx
    • \user
      • login.aspx
    • \support
      • help.aspx
    • \usercontrols
      • header.ascx
    • default.aspx

All of the ASPX files use the same "header.ascx" control (I'm not using master pages on this site).  The "header.ascx" control needs to reference the img, but note that the relative path is calculated based on the page requested, not the user control requested.  This means that the relative URL will only work if the user control just happens to be included in a page at the correct depth.  All other times it will break, and this is a huge problem.

Luckily, ASP.NET includes a handy way to fix all of these problems, deployment and otherwise.

Application root

A path built with an application root is prefixed with a tilde (~).  For example, here's a raw application path to the image:

~/img/blarg.jpg

Note the "~/" at the front, that's what signifies it as an application root path.  Application root paths are constructed starting at the root of the application.  For example, both "http://ecommapp.com" and "http://localhost/EcommApp" are application roots, so I don't have to worry about changing paths at deployment.

Additionally, I don't have to worry about problems with node depth in the hierarchy, as paths are formed from the root and not relative to a leaf node, so my user control problem disappears.

One issue with application root paths is only ASP.NET knows about them.  Not browsers.  If I do this:

<img src="~/img/blarg.jpg" />

The image breaks, as browsers don't know what IIS applications are, they just know URLs.  ASP.NET, however, will take this URL and generate the correct relative path for you, as long as I use ASP.NET to generate the path.  Server controls, like "asp:hyperlink" can handle the application root path.

To use the application root path in raw HTML, I just need to use the ResolveUrl method, which is included in the Control class, and therefore available in both my Page and UserControl classes.  Combining raw HTML and the ResolveUrl method, I get:

<img src="<%= ResolveUrl("~/img/blarg.jpg") %>" />

The "<%= %>" construct is basically a "Response.Write", and allows me to call the ResolveUrl method directly.

Using application root paths allows me to:

  • Develop locally and deploy to production seamlessly
  • Have consistent URL per resource
  • Use raw HTML without the problems of absolute and relative paths

Some caveats

No matter what I do, I have to change code if either the resource or the page moves.  I can minimize the number of changes by externalizing the specific path (the "~/img/blarg.jpg" part) to a resource file, constant, or static global variable.  This applies for all types of paths, so I like to eliminate as much duplication as possible.

It's dangerous to assume that the structure or names of resources and pages won't change at some point.  As a web site grows, it can become necessary to move resources around and reorganize your site structure.  To minimize the impact of deployment and change, use application paths as much as possible, you'll save on Excedrin later.

3 comments:

Gabe Moothart said...

Good advice. Incidentally, the CssHandler project on codeplex allows you to (among other things) use app-relative paths in css files.

Anonymous said...

You can also use runat="server" in your img tag instead of ResolveUrl.

Jimmy Bogard said...

@gabe

Thanks! I've jumped through sooooo many hoops to get that in the past.

@sirmike
Yep, that'll also work, as it makes it an HTML server control.