Click here to Skip to main content
15,861,168 members
Articles / Web Development / CSS
Article

Using the FileResolver to allow virtual application paths ( ~ ) in any file

Rate me:
Please Sign up or sign in to vote.
4.66/5 (17 votes)
10 Jan 20065 min read 112.2K   643   49   24
Introduces a solution for using virtual app paths in non ASP.NET files.

Introduction

If you are not familiar with tilde usage in ASP.NET, then you are totally missing out. Often, our development environment is totally different than a production server. We may test our application using a virtual directory on our development machine, but may publish it to a dedicated root site.

So when you add an image or a link, you must always be aware of the type of the path you provide - relative, absolute, etc. Well, one of the best little tricks of ASP.NET is the tilde (~). This is essentially a shortcut for the HttpRuntime.AppDomainAppVirtualPath property, which refers to the virtual application root, not the root of the web server.

While the tilde is quick and clean, it's also quite limited. For one, it is only useful when setting paths for certain controls that know how to resolve the path before rendering. So not only is it useful only in pre-defined areas of ASP.NET, it absolutely cannot be used outside any file not managed by the ASP.NET process. Until now.

One of the problems I often run into is when I am setting a path for something in a non ASP.NET file, such as a CSS file. For instance, I want to set the background-image attribute for a style in a CSS file, but unless I use a relative path, I’m screwed. I would usually end up just adding the background-image attribute directly to the tag instead of putting it into a CSS file. I’ll be the first to admit this is a lame workaround.

A couple ideas on how to solve this problem came to my mind. After some collaboration, a solid solution was established. The idea consists of using HTTP Handlers to intercept any requests made to resources, such as a CSS, or even a JavaScript file. The handler is then responsible for parsing the said file and delivering a "resolved" file to the client.

Background

This article assumes a basic understanding of HTTP Handlers.

Using the code

For the sake of this article, I'm going to use CSS files as an example, although this technique could easily be applied to any type of file whose contents need to be resolved.

Currently, one might use the following tag to link a stylesheet to a .aspx page.

HTML
<link rel="stylesheet" href="resources/stylesheet.css" />

Once I've implemented the solution below, the same link tag will look like this:

HTML
<link rel="stylesheet" href="~/resources/stylesheet.css.ashx" />

As you can see, there was a minor change with the appendage of ".ashx" onto the file path. This tells the FileResolver to resolve any virtual app path inside this CSS file.

Another small change is that now we can use the tilde in the file path for the CSS file. As you'll see later on, the HTTP Handler will resolve this path for the CSS file automatically. However, this does not seem to be the case in ASP.NET 1.1. So for ASP.NET 1.1, you may have to use a real relative or absolute path.

So, let's take a look at the guts of the FileResolver handler.

C#
namespace FileResolverDemoWeb
{
    public class FileResolver : IHttpHandler
    {
        /// <summary>
        /// File cache item used to store file
        /// content & date entered into cache
        /// </summary>
        internal class FileCacheItem
        {
            internal string Content;
            internal DateTime DateEntered = DateTime.Now;

            internal FileCacheItem(string content)
            {
                this.Content = content;
            }
        }

        private FileCacheItem UpdateFileCache(HttpContext context, 
                                                  string filePath)
        {
            string content;

            using(FileStream fs = new FileStream(filePath, 
                             FileMode.Open, FileAccess.Read))
            {
                using(StreamReader sr = new StreamReader(fs))
                {
                    content = sr.ReadToEnd();
                    sr.Close();
                }

                fs.Close();
            }

            //Get absolute application path
            string relAppPath = HttpRuntime.AppDomainAppVirtualPath;
            if(!relAppPath.EndsWith("/"))
                relAppPath += "/";

            //Replace virtual paths w/ absolute path
            content = content.Replace("~/", relAppPath);

            FileCacheItem ci = new FileCacheItem(content);

            //Store the FileCacheItem in cache 
            //w/ a dependency on the file changing
            CacheDependency cd = new CacheDependency(filePath);
            context.Cache.Insert(filePath, ci, cd);
            return ci;
        }

        public void ProcessRequest(HttpContext context)
        {
            string absFilePath = 
                context.Request.PhysicalPath.Replace(".ashx", "");
            
            //If a tilde was used in the page 
            //to this file, replace it w/ the app path
            if(absFilePath.IndexOf("~\\") > -1)
                absFilePath = absFilePath.Replace("~", 
                              "").Replace("\\\\", "\\");

            if(!File.Exists(absFilePath))
            {
                context.Response.StatusCode = 404;
                return;
            }

            FileCacheItem ci = (FileCacheItem)context.Cache[absFilePath];
            if(ci != null)
            {
                if(context.Request.Headers["If-Modified-Since"] != null)
                {
                    try
                    {
                        DateTime date = DateTime.Parse(
                          context.Request.Headers["If-Modified-Since"]);

                        if(ci.DateEntered.ToString() == date.ToString())
                        {
                            //Don't do anything, nothing 
                            //has changed since last request
                            context.Response.StatusCode = 304;
                            context.Response.StatusDescription = 
                                                 "Not Modified";
                            context.Response.End();
                            return;
                        }
                    }
                    catch(Exception){}
                }
                else
                {
                    //In the event that the browser doesn't 
                    //automatically have this header, add it
                    context.Response.AddHeader("If-Modified-Since", 
                                        ci.DateEntered.ToString());
                }
            }
            else
            {
                //Cache item not found, update cache
                ci = UpdateFileCache(context, absFilePath);
            }

            context.Response.Cache.SetLastModified(ci.DateEntered);
            context.Response.ContentType = "text/" + 
                    GetContentType(Path.GetExtension(absFilePath));
            context.Response.Write(ci.Content);
            context.Response.End();
        }

        /// <summary>
        /// Gets the appropriate content type for a specified extension
        /// </summary>
        private string GetContentType(string ext)
        {
            switch(ext.ToLower())
            {
                case ".css":
                    return "css";
                    break;
                case ".xml":
                    return "xml";
                    break;
                case ".js":
                    return "javascript";
                    break;
                default:
                    return "plain";
                    break;
            }
        }

        #region IHttpHandler Members

        public bool IsReusable
        {
            get
            {
                return true;
            }
        }

        #endregion
    }
}

Now, let's look at each part of the preceding code.

CacheItem is an internal class designated to hold the resolved content of the CSS file. It also has a DateEntered property tied to the content that is set to the last date and time the content was updated. This is useful in determining whether or not we need to serve up a new, fresh version of the CSS file content to a client.

ProcessRequest is a method that must be implemented for any class that implements the IHttpHandler. It also contains the bulk of the logic for this handler.

In the ProcessRequest method, we know what file is being handled from the HttpContext.Request.PhysicalPath property. We will do an initial check to ensure that the path to the file has been resolved. Once we have the physical, mapped path of the file, we do an initial check just to make sure the file still exists on the file system.

C#
string absFilePath = context.Request.PhysicalPath.Replace(".ashx", "");

//If a tilde was used in the page to this file, replace it w/ the app path
if(absFilePath.IndexOf("~\\") > -1)
    absFilePath = absFilePath.Replace("~", "").Replace("\\\\", "\\");

if(!File.Exists(absFilePath))
{
    context.Response.StatusCode = 404;
    return;
}

Once verified, we need to check the page's cache to see if a related CacheItem has already been added. Once an initial request is made to this CSS file, a CacheItem is created and stored.

If the CacheItem exists, we compare the DateEntered value against the If-Modified-Since header value from the request. If the dates match, then we know that the client has the latest version of the file already cached on their end. If the dates don't match or no appropriate header is found, we attempt to add the header and will write fresh content to the client.

C#
FileCacheItem ci = (FileCacheItem)context.Cache[absFilePath];
if(ci != null)
{
    if(context.Request.Headers["If-Modified-Since"] != null)
    {
        try
        {
            DateTime date = DateTime.Parse(
                 context.Request.Headers["If-Modified-Since"]);

            if(ci.DateEntered.ToString() == date.ToString())
            {
                //Don't do anything, nothing has 
                //changed since last request
                context.Response.StatusCode = 304;
                context.Response.StatusDescription = "Not Modified";
                context.Response.End();
                return;
            }
        }
        catch(Exception){}
    }
    else
    {
        //In the event that the browser doesn't 
        //automatically have this header, add it
        context.Response.AddHeader("If-Modified-Since", 
                            ci.DateEntered.ToString());
    }
}
else
{
    //Cache item not found, update cache
    ci = UpdateFileCache(context, absFilePath);
}

context.Response.Cache.SetLastModified(ci.DateEntered);
context.Response.ContentType = "text/" + 
        GetContentType(Path.GetExtension(absFilePath));
context.Response.Write(ci.Content);
context.Response.End();

If the CacheItem is not found, we need to update the cache with a new CacheItem. This consists of reading the content from the CSS file and replacing any instance of the tilde shortcut with a true application path. Then, we'll package the new content into a CacheItem and store it in the page cache.

C#
private FileCacheItem UpdateFileCache(HttpContext context, 
                                          string filePath)
{
    string content;

    using(FileStream fs = new FileStream(filePath, 
                          FileMode.Open, FileAccess.Read))
    {
        using(StreamReader sr = new StreamReader(fs))
        {
            content = sr.ReadToEnd();
            sr.Close();
        }

        fs.Close();
    }

    //Get absolute application path
    string relAppPath = HttpRuntime.AppDomainAppVirtualPath;
    if(!relAppPath.EndsWith("/"))
        relAppPath += "/";

    //Replace virtual paths w/ absolute path
    content = content.Replace("~/", relAppPath);

    FileCacheItem ci = new FileCacheItem(content);

    //Store the FileCacheItem in cache 
    //w/ a dependency on the file changing
    CacheDependency cd = new CacheDependency(filePath);
    context.Cache.Insert(filePath, ci, cd);
    return ci;
}

That's pretty much all there is to it. Like all handlers, they won't work until you add a few additional entries to your web.config. Not only is this necessary, but this is where we get to do a little configuration to extend the FileResolver to support any file types we want.

XML
<configuration>
    <system.web>
        <httpHandlers>
            <add verb="GET" path="*.css.ashx" 
                  type="FileResolverDemoWeb.FileResolver,FileResolverDemoWeb" />
            <add verb="GET" path="*.js.ashx" 
                  type="FileResolverDemoWeb.FileResolver,FileResolverDemoWeb" />
        </httpHandlers>
    </system.web>
</configuration>

In this scenario, I've chosen to resolve the content for the CSS as well as the JavaScript files. Good times.

Credits

Props to Jon Gilkison for the collaboration efforts leading to this solution.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
I currently work for a company in San Diego, CA authoring server controls.

Get the latest up to date code at http://www.csharper.net

Comments and Discussions

 
GeneralVB Pin
carlor7-Mar-09 0:30
carlor7-Mar-09 0:30 
GeneralProblems with apache forwarding and a trailing slash Pin
jakeschirm2-Dec-08 10:53
jakeschirm2-Dec-08 10:53 
GeneralVery Helpful Pin
Mike Ellison20-May-08 6:08
Mike Ellison20-May-08 6:08 
GeneralBetter way of resolving relative urls Pin
M. Manovich9-Oct-07 7:58
M. Manovich9-Oct-07 7:58 
GeneralChange to example.. Problem with Subfolder Call Pin
MelvinB9-Jan-07 3:47
MelvinB9-Jan-07 3:47 
GeneralCannot make it to work as &lt;link rel="stylesheet" href="~/resources/stylesheet.css.ashx" /> Pin
vlad19791-Jun-06 11:03
vlad19791-Jun-06 11:03 
QuestionHow can I resolve my Server Control's virtual path at design time? Pin
yg700113-Apr-06 21:15
yg700113-Apr-06 21:15 
GeneralTheme Support Pin
lostxp1-Mar-06 23:49
lostxp1-Mar-06 23:49 
Questionhow to modify for handling .gif or .jpg Pin
Rac_1232-Feb-06 6:14
Rac_1232-Feb-06 6:14 
Question.htc Type? Pin
IgDev27-Jan-06 9:57
IgDev27-Jan-06 9:57 
AnswerRe: .htc Type? Pin
IgDev27-Jan-06 10:10
IgDev27-Jan-06 10:10 
QuestionHow to use it? Pin
placek19-Jan-06 0:33
placek19-Jan-06 0:33 
AnswerRe: How to use it? Pin
Bobby DeRosa19-Jan-06 4:17
Bobby DeRosa19-Jan-06 4:17 
QuestionParse ~'s in a file? Pin
rball18-Jan-06 8:48
rball18-Jan-06 8:48 
AnswerRe: Parse ~'s in a file? Pin
Bobby DeRosa18-Jan-06 9:43
Bobby DeRosa18-Jan-06 9:43 
Yes they will. That's basically the entire concept behind this approach - so that you can use ~ in any non ASP.NET file, like .css & .js.

Cheers,

-- Slightly
GeneralRe: Parse ~'s in a file? Pin
n1ck0s19-Jan-06 23:58
n1ck0s19-Jan-06 23:58 
GeneralNice 1 :) Pin
J. Gilkison17-Jan-06 11:05
J. Gilkison17-Jan-06 11:05 
Generaldo not works with template Pin
carzel13-Jan-06 4:04
carzel13-Jan-06 4:04 
GeneralRe: do not works with template Pin
Bobby DeRosa13-Jan-06 4:22
Bobby DeRosa13-Jan-06 4:22 
QuestionFileResolver withURLRewrite? Pin
Preky11-Jan-06 3:19
Preky11-Jan-06 3:19 
AnswerRe: FileResolver withURLRewrite? Pin
Bobby DeRosa11-Jan-06 5:09
Bobby DeRosa11-Jan-06 5:09 
GeneralRe: FileResolver withURLRewrite? Pin
Preky11-Jan-06 20:50
Preky11-Jan-06 20:50 
GeneralRe: FileResolver withURLRewrite? Pin
Bobby DeRosa12-Jan-06 5:35
Bobby DeRosa12-Jan-06 5:35 
GeneralRe: FileResolver withURLRewrite? Pin
Preky12-Jan-06 21:39
Preky12-Jan-06 21:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.