Asked by Chris Sells. Answered by the Wonk on January 2, 2003
A.
This question comes up again and again. In building my own web site, I've often found the need to compose path names to resources on my site. For example, the image at the top of each of my web pages is determined dynamically based on what directory you're reading from. So, if you're reading a page from tools or one of the sub-directories, the src attribute of the <img> tag is:
http://www.sellsbrothers.com/images/tg_tools.gif
(where “tg” stands for “top graphic”). At least, that's what you get when you surf to my public web site. However, when I'm testing my web site internally before publishing it, the URL for the image is very different
http://localhost/sb/images/tg_tools.gif
Notice that in test mode, my web site is just another vdir and not the default web. And therein lies a problem. I can't just slap my domain name on the front of every URL – I need each URL on my site to be relative to the current site I’m testing, otherwise I’ll follow a link from my test site to my live site and not be testing anymore.
Of course, the immediately obvious solution is to use relative path names instead of absolute path names. Unfortunately, that doesn’t work as nicely as you’d like, either. The problem is that I’m using a single web control on every page of my web site to give it a common look 'n' feel. The web control will come into existence at all levels of my site:
/ /tools /tools/monikers
I can't just use a hard-coded relative link to get to the image from the control, because even the relative path needs to be adjusted dynamically:
/images/tg_tools.gif ../images/tg_tools.gif ../../images/tg_tools.gif
The HttpRequest class provides a very promising method called MapPath that sounds useful to solve this problem. Unfortunately, MapPath maps to the physical location on the hard drive, not to an URL:
// default.aspx
// yields d:\project\mine\sb\images\tg_tools.gif
string src = request.MapPath(“images/tg_tools.gif”);
Even if MapPath gave me an URL, it will only map from the current sub-dir and not from the root, so if I'm three levels deep, there’s no way to get a path relative to the root:
// foo/bar/default.aspx
// yields d:\project\mine\sb\foo\bar\images\tg_tools.gif
// and not d:\project\mine\sb\images\tg_tools.gif
string src = request.MapPath(“/images/tg_tools.gif”);
What we're really after is another kind of relative than we’ve had so far. We know how to build absolute path names and URL names starting from some known root. We also know how to build relative path and URL names starting from where we are. We need something else. To abstract away the differences between web servers, we need to be able to construct absolute path and URL names relative to the logical root of the vdir. No matter where I am in the hierarchy of my web site, I need to be able to translate:
images/tg_tools.gif
into:
http://localhost/sb/images/tg_tools.gif
when in test mode and into:
http://www.sellsbrothers.com/images/tg_tools.gif
when in release mode, but not have to change my code to do it (I tried that once – wow what a lot of work). ASP.NET provides no build in classes or methods that will do this for you. Luckily, it provides the necessary building blocks.
The core of mapping as relative from a logical root is to first find the logical root. This can be done using the HttpRequest URL property and HttpRequest ApplicationPath property:
public class WebPathHelper {
// "http://www.foo.com/" or "http://localhost/foo/"
public static string RootUrl {
get {
HttpRequest request = HttpContext.Current.Request;
// "http://www.foo.com/foo.htm" or "http://localhost/foo/foo.htm"
string
requestUrl = request.Url.ToString();
// "http" or "ftp"
string
protocol = requestUrl.Substring(0, requestUrl.IndexOf(":"));
// "www.foo.com" or "localhost"
string site = Site.CreateFromUrl(requestUrl).Name;
// "/" or "/foo" => "" or "foo"
string appPath = request.ApplicationPath.Substring(1);
// "http://www.foo.com/" or "http://localhost/foo/"
string
root = string.Format(
"{0}://{1}/{2}{3}",
protocol,
site,
appPath,
appPath.Length > 0 ? "/" : "");
Debug.Assert(root.EndsWith("/"));
return root;
}
}
...
}
No matter where WebPathHelper.RootUrl is called from, it will use the current HttpRequest object as context to return the root of the web site as an URL, including any vdir name as needed, as shown here:
Getting RootUrl From |
V-Dir Name |
WebPathHelper.RootUrl |
http://www.sellsbrothers.com |
<none> |
http://www.sellsbrothers.com/ |
http://www.sellsbrothers.com/foo |
<none> |
http://www.sellsbrothers.com/ |
http://localhost/sb |
/sb |
Sb |
http://localhost/sb/foo |
sb |
http://localhost/sb |
From this, it's easy to construct a mapping function:
public class WebPathHelper {
public static string MapUrlFromRoot(string url) {
string safeUrl = url.Replace('\\', '/').TrimStart('/');
return RootUrl + safeUrl;
}
...
}
Now, I can call WebPathHelper.MapUrlFromRoot("images/tg_tools.gif") from anywhere on my site and always get the right URL, whether it’s on my test site or my live site.
When I have similar needs on the file system, e.g. loading an Access database, I can do so using RootPath and MapPathFromRoot:
public class WebPathHelper {
public static string RootPath {
get {
HttpRequest request = HttpContext.Current.Request;
string root = request.PhysicalApplicationPath;
Debug.Assert(root.EndsWith(@"\"));
return root;
}
}
public static string MapPathFromRoot(string path) {
string safePath = path.Replace('/', '\\').TrimStart('\\');
return RootPath + safePath;
}
...
}
This allows me to call WebPathHelper.MapPathFromRoot(@"foo\foo.mdb") and get the correct path name, where it's on my test machine or whether it’s on whatever machine ORCSWeb is using to host my site.
Where Are We?
In exploring this problem, I discovered that there are three kinds of paths, where only two have come up explicitly in my previous experience. We’re all familiar with absolute and relative URL and file names and our operating systems and browsers can consume them w/o trouble. However, building web applications requires also building absolute URLs and files from URLs and files relative to a logical root. The WebPathHelper will construct paths relative to the logical vdir root, which fills in the functionality missing from .NET for dealing with only relative and absolutely paths. This lets me add features to my site that require URLs and file names, test them locally and have confidence that they’ll work exactly the same way on my production site.
This one was a pain to figure out. I started by noticing the problem and attempting some bone-headed temporary solutions that I'd prefer not to list here (too embarrassing : ). The key came when I asked some of my ASP.NET expert friends and they agreed that there was no good way to do what I wanted built into ASP.NET itself. Once that was established, I started digging through the information that ASP.NET, particularly the Request object, provided, which is where the aspx pages in the sample came from. From that information, I pulled out what I needed to build the WebPathHelper class and then deployed it on my own site, fixed the bugs that can only be found in a real-world deployment.
I don't know that I could have avoided this problem. I had actual resources, both file and URL, to reference that were relative to the root of the vdir. It would've been better to dig into the problem for real in the first place instead of my silly bandaids, but one can't always do things the "right" way the first time. The key was to recognize that I can a recurring problem happening in all kinds of spots on my web site, figuring out that they were all the same problem and sitting down to fix it once and for all.
Jamie Cansdale pointed out that the ~ character can be used in place of Request.ApplicationPath in some limited cases, e.g.
<a runat="server" href="~/images/tg_tools.gif">relative to virtual root</a>
However, this shortcut can only be used with server-side controls and only in a few places. I prefer the WebPathHelper because it's explicit and general purpose.
Damjan Janevski has the following feedback:Page.ResolveUrl("~/images/tg_tools.gif") will return string with URL relative to the web application folder (vdir).
e.g. for http://www.sellsbrothers.com/images/tg_tools.gif it will be "/images/tg_tools.gif", for http://localhost/sb/images/tg_tools.gif it will be "/sb/images/tg_tools.gif".
It's better from WebPathHelper solution because the links are relative and no requests to DNS server are made.
Harry Collins points out that I forgot to handle the port specification, e.g. http://192.168.1.47:12300/mypage.aspx.