Using Virtual Application Paths ( ~ ) in Any File!
posted on Friday, December 30, 2005 by bobby @ 6:00 am
Earlier today, I posted an article about using virtual application paths inside a css file. While that approach is indeed a step in the right direction, I wasn't quite happy w/ the way I had implemented it - specifically the fact that a new file would be spawned alongside the real css file, with a "_resolved.css" appendage.

This approach only works if you give ASP.NET full access to write, and even then, it's easy for a file to get locked, resulting in a fatty exception.

Jon Gilkison had a great idea to use HttpHandlers to intercept the request & intelligently cache the resolved css in memory. I took his code and slightly modified it to what you see below. This is by far the best approach. Props to Jon for writing the bulk of HttpHandler code.

Once employed, this is what your css links in markup will look like:
Copy code to clipboard in IE or select code for Firefox
<link rel="stylesheet" href="~/resources/ui/all.css.ashx" />
<link rel="stylesheet" href="~/resources/ui/msie.css.ashx" />


Notice that the only change is the additional ".ashx" applied to the end of the file name. The ".ashx" exists to distinguish this resource from other ".js" files that should not get resolved. No controls to use or register - very clean & mad stupid dope.

This is the FileResolver class - the HttpHandler responsible for intercepting any calls requesting (in this case) .css.ashx files (could be any file - .myExtension.ashx).

Warning: Lots of code below. Copy, build, rock on.
Copy code to clipboard in IE or select code for Firefox
namespace Oxford.Web
{
    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", "");
            //Ensure ~/ in file path is resolved
            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
    }
}


Since we are using HttpHandlers, you will need to add one entry to your web.config file.
Copy code to clipboard in IE or select code for Firefox
...
<httpHandlers>
    <add verb="GET" path="*.css.ashx" type="Oxford.Web.FileResolver,Oxford.Web" />
</httpHandlers>
...


Another benefit to this approach is that you can use it for any file type, not just css. Simply add additional line entries to your web.config for other types, like .js. I tested with a javascript file and it worked like a charm.
Copy code to clipboard in IE or select code for Firefox
...
<httpHandlers>
    <add verb="GET" path="*.css.ashx" type="Oxford.Web.FileResolver,Oxford.Web" />
    <add verb="GET" path="*.js.ashx" type="Oxford.Web.FileResolver,Oxford.Web" />
</httpHandlers>
...


Likewise, to add the script tag to your markup would look like this:
Copy code to clipboard in IE or select code for Firefox
<script lang="javascript" src="~/resources/script/script.js.ashx"> </script>


"You take yourself out of the game, you start talking about puppy dogs and ice cream and of course it's going to end up on the friendship tip." - DoubleDown, Swingers

CommentsComments
posted on Tuesday, January 03, 2006  by Anonymous @ 10:34 AM

This is a great idea, and is similar to the resource embedding technqiue that asp.net 2.0 offers ONLY for control assemblies. This makes it much easier where you dont want to wrap everysing up in controls.

Questions: This line of code fails for all resources
Copy code to clipboard in IE or select code for Firefox
string absFilePath = context.Request.PhysicalPath.Replace(".ashx", "");

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


The file never exists for any resources because the ~ is still in the path.

2. This technique (if i can get it working) would also allow css files that have images hrefs inside them to also use the .ashx technqiue.

Regards

Gerard
posted on Tuesday, January 03, 2006  by bobby @ 11:10 AM

Hey Gerard,

You are correct. You will need to use a real virtual or relative path for your <link /> tag. This works for me because I employ this trick on my site.

On the other hand, if you wanted to use a resolved path all the time w/out employing the latter technique, you could easily turn this into a control. See the first part of this post.

Cheers,
Bobby
posted on Tuesday, January 03, 2006  by Anonymous @ 11:52 AM

Bobby,

thansk for quick reply.

Firstly, i understand what you are doing in the other post you refer me to.

Does it not make sense however to simply chane the code to work with the "~" automatically. This works but i have not tested it fully on sites that dont have a virtual dir at the start (i.e production deployment scenario).
Copy code to clipboard in IE or select code for Firefox
public void ProcessRequest(HttpContext context)
{
    //Create full physical path( assuming "~" used)
    //build prefix
    string appRootPath = context.Request.PhysicalApplicationPath;
    //build suffix
    int startIndex = context.Request.PhysicalPath.IndexOf("~") + 2;
    int endIndex = context.Request.PhysicalPath.Length;
    string resourceAbsolutePath = context.Request.PhysicalPath.Substring(startIndex, endIndex - startIndex);

    string absFilePath = (appRootPath + resourceAbsolutePath);
    absFilePath = absFilePath.Replace(".ashx", "");
}

posted on Tuesday, January 03, 2006  by bobby @ 12:10 PM

I have to retract my previous statement - this should work regardless of the "resolve path anywhere" trick. I tried this on a regular aspx page (one that inherits from System.Web.UI.Page) and the context.Request.PhysicalPath property returns a fully resolved path to the proper file. I didn't do anything special at all - it simply works.

Of course you can add that code, but it doesn't seem you like you should really have to.

Cheers,
Bobby
posted on Tuesday, January 03, 2006  by Anonymous @ 12:15 PM

Bobby,

Now i see what you meant at the start. sorted - sorry about confusion.

i am using this for a complex Ajax based project. It is really doing ym head in.
Almost considering to port the current implemenation to Flash (using the Eclipse IDE), as its such a dog doing this untyped code.
posted on Tuesday, January 03, 2006  by bobby @ 12:29 PM

heh, no worries. Yeah, I know what you mean about Ajax. I decided to just wait for Atlas to come out instead of trying to do anything complex in Ajax just yet.
posted on Thursday, January 05, 2006  by bobby @ 3:27 AM

Another reason the ~/ may have not resolved properly in the <link /> tag is because the <head /> tag may not have had the runat="server" attribute tied to it.

The <head /> tag must be interpreted by the server to use virtual app paths for the href attribute. If you cannot do this within your application, you must use a real relative or absolute path.
posted on Thursday, January 05, 2006  by Gerard @ 8:57 PM

Hey Bobby,

i have rewritten all the code to allow the "~" technique to be used for text AND binary files.

I needed this because i wanted my binaries to also do this.

The caching can be turned on or off.
Typically you want the text files to be cached but NOT the binary. This is because the text can have embedded paths that need to be converted (which i now do also), and hence you may as well do this.
Its important to be able to turn caching complete off during development, because when you change a file (like JS) you want to see its effect. Caching is a HUGE pain here.

i can send it to you and you can put it on your blog if you want mate. Just give me some credit if you want :)

Gerard
posted on Friday, January 06, 2006  by bobby @ 5:29 AM

Gerard,

Feel free to post the code as a comment, just use the [pre] tags and the code will automatically be color coded.

Cheers,
Bobby
posted on Friday, January 06, 2006  by Anonymous @ 7:20 AM

Bobby,

Heres my altered code. It allows any resource to use the ashx extension with the "~" suffix.
Bloody useful for any web project IMHO.
Copy code to clipboard in IE or select code for Firefox
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.IO;

namespace Ubu.Framework.Web
{

    public class FileResolver : IHttpHandler
    {

        #region Caching
        /// &lt;summary&gt;
        /// File cache item used to store file content & date entered into cache
        /// &lt;/summary&gt;
        /// &lt;example&gt;
        /// HTML:
        /// &lt;link rel="stylesheet" href="~/resources/ui/all.css.ashx" /&gt;
        /// web.config:
        /// &lt;httpHandlers&gt;
        /// &lt;add verb="GET" path="*.css.ashx" type="Oxford.Web.FileResolver,Oxford.Web" /&gt;
        /// &lt;add verb="GET" path="*.js.ashx" type="Oxford.Web.FileResolver,Oxford.Web" /&gt;
        /// &lt;/httpHandlers&gt;
        /// &lt;/example&gt;
        internal class FileCacheItemImage
        {
            internal byte[] Content;
            internal DateTime DateEntered = DateTime.Now;

            internal FileCacheItemImage(byte[] content)
            {
                this.Content = content;
            }

        }

        private FileCacheItemImage UpdateFileCacheImage(HttpContext context, byte[] content, string cacheKey)
        {

            FileCacheItemImage ci = new FileCacheItemImage(content);

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




        internal class FileCacheItemText
        {
            internal string Content;
            internal DateTime DateEntered = DateTime.Now;

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


        }

        private FileCacheItemText UpdateFileCacheText(HttpContext context, string content, string cacheKey)
        {

            FileCacheItemText ci = new FileCacheItemText(content);

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

        public void ClearCache(HttpContext context)
        {
            List&lt;string&gt; keyList = new List&lt;string&gt;();
            System.Collections.IDictionaryEnumerator CacheEnum = context.Cache.GetEnumerator();
            while (CacheEnum.MoveNext())
            {
                keyList.Add(CacheEnum.Key.ToString());
            }
            foreach (string key in keyList)
            {
                context.Cache.Remove(key);
            }
        }

        #endregion

        public void ProcessRequest(HttpContext context)
        {


            /// Create full physical path( assuming "~" used always ) to normal path
            /// so we get grab the file.
            // build prefix
            string appRootPath = context.Request.PhysicalApplicationPath;
            // build suffix
            int startIndex = context.Request.PhysicalPath.IndexOf("~") + 2;
            int endIndex = context.Request.PhysicalPath.Length;
            string resourceAbsolutePath = context.Request.PhysicalPath.Substring(startIndex, endIndex - startIndex);


            string absFilePath = (appRootPath + resourceAbsolutePath);
            absFilePath = absFilePath.Replace(".ashx", "");

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

            if (GetContentType(Path.GetExtension(absFilePath)).Contains("text") == false)
            {
                this.ProcessRequestedImage(context, absFilePath);
            }
            else
            {
                this.ProcessRequestedText(context,absFilePath);
            }


        }


        private void ProcessRequestedImage(HttpContext context, string absFilePath)
        {
            bool useCache = true;
            string cacheKey = absFilePath;
            Byte[] imageBytes = new byte[0];

            // Get data
            if (useCache)
            {
                // Check if the cache contains the image.
                Object cachedObject = context.Cache.Get(cacheKey);

                if (cachedObject != null)
                {
                    imageBytes = (Byte[])cachedObject;
                }
            }

            // check is not found in cache
            if (imageBytes.Length == 0)
            {
                imageBytes = this.PullDataAsBinary(context, absFilePath);

                if (useCache)
                {
                    // Store it in the cache (to be expired after 2 hours).
                    context.Cache.Add(cacheKey, imageBytes, null, DateTime.MaxValue, new TimeSpan(2, 0, 0), CacheItemPriority.Normal, null);
                }
            }

            // Send back in Response.
            context.Response.ContentType = "image/jpeg";
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            context.Response.Cache.SetLastModified(DateTime.Now);
            context.Response.BufferOutput = false;
            context.Response.OutputStream.Write(imageBytes, 0, imageBytes.Length);
            //context.Response.End;
        }

        private void ProcessRequestedText(HttpContext context, string absFilePath)
        {
            bool useCache = false;
            string cacheKey = absFilePath;
            string content = string.Empty;

            // Get data
            if (useCache)
            {
                // Check if the cache contains the image.
                Object cachedObject = context.Cache.Get(cacheKey);

                if (cachedObject != null)
                {
                    content = (string)cachedObject;
                }
            }

            // check is not found in cache
            if (content.Length == 0)
            {
                content = this.PullDataAsText(context, absFilePath);
                content = this.ParseData(content);  // to ensure .ashx paths are converted also

                if (useCache)
                {
                    // Store it in the cache (to be expired after 2 hours).
                    context.Cache.Add(cacheKey, content, null, DateTime.MaxValue, new TimeSpan(2, 0, 0), CacheItemPriority.Normal, null);
                }
            }

            context.Response.Cache.SetLastModified(DateTime.Now);
            context.Response.ContentType = GetContentType(Path.GetExtension(absFilePath));
            context.Response.Write(content);
            context.Response.End();
        }

        private string ParseData(string contentString)
        {

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

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

            // replace ".ashx" with nothing
            contentString = contentString.Replace(".ashx", "");


            return contentString;
        }

        private byte[] ParseData(byte[] contentBytes)
        {
            byte[] resultBytes;
            string contentString;

            // convert to String
            System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
            contentString = enc.GetString(contentBytes);

            contentString = this.ParseData(contentString);

            // convert back to Bytes
            System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
            resultBytes = encoding.GetBytes(contentString);

            return resultBytes;
        }

        private string PullDataAsText(HttpContext context, string filePath)
        {
            string data;

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

            return data;
        }

        private Byte[] PullDataAsBinary(HttpContext context, string filePath)
        {
            byte[] data;

            using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                data = new byte[fs.Length];
                fs.Read(data, 0, data.Length);
                fs.Close();
            }

            return data;
        }



        /// &lt;summary&gt;
        /// Gets the appropriate content type for a specified extension
        /// &lt;/summary&gt;
        private string GetContentType(string ext)
        {
            switch (ext.ToLower())
            {
                case ".css":
                    return "text/css";
                case ".xml":
                    return "text/xml";
                case ".js":
                    return "text/javascript";
                case ".png":
                    return "image/png";
                default:
                    throw new NotSupportedException("The File extension of [" + ext + "] is not supported.");
            }
        }

        #region IHttpHandler Members

        public bool IsReusable
        {
            get
            {
                return true;
            }
        }

        #endregion
    }
}

posted on Tuesday, January 24, 2006  by Steven Tapping @ 6:22 AM

Just came accross your post... have you tried out Development Server? It's shipped with Visual Studio 2005. It will allow you to develop on root on your XP box the same way you would deploy on IIS 6. So your images references would start from \images instead of \YourVirtualDir\images. Same goes with css, js, etc.

Check out this post for how to configure it:
http://weblogs.asp.net/scottgu/archive/2005/11/21/431138.aspx

If you web project is already configured to use IIS, it only takes a a few lines of code to change in your solution file to use Dev Server.
posted on Wednesday, January 25, 2006  by Anonymous @ 5:22 AM

Odd. I have the HTTPHandler setup (ASP.NET 2.0), and renamed my .css to .css.ashx.

But the compiler complains:
Error 1 The page must have a <%@ webhandler class="MyNamespace.MyClass" ... %> directive. c:\testweb\styles\default.css.ashx 1

What should a shiznit-friendly .css file look like?

Thanks.
posted on Tuesday, February 28, 2006  by Anonymous @ 10:25 PM

I get the same error. How to write js.ashx files. Can you give sample..
posted on Wednesday, March 01, 2006  by bobby @ 12:16 AM

Have you already looked at the Code Project article dealing w/ this topic? If not, check it out - there are working projects to download for 2003 & 2005.

http://www.codeproject.com/aspnet/UsingTheFileResolver.asp

Cheers,
Bobby
posted on Wednesday, March 01, 2006  by Anonymous @ 8:20 PM

what about asp.net 2.0 themes. Does it support ?
There is an App_Themes folder all my css files there. And these files automatically put in every files. how can change their extesion?
posted on Sunday, April 16, 2006  by bobby @ 3:34 PM

No, I haven't done any testing to ensure the support of themes.
posted on Friday, June 16, 2006  by vip32 @ 3:43 PM

this works just as good, no need to rename your .js and .css files to .xxx.ashx
<add verb="GET" path="*.css" type="Macaw.Web.Handlers.ResolveAppVirtualPathHandler, Macaw.Web" />
the handler picks it up if you change the config to this. why did you put an ashx after the resource files?

even .css files in themes are supported this way
posted on Friday, June 16, 2006  by bobby @ 3:50 PM

The .ashx extension is necessary to distinguish which js/css files you want resolved. If I remove the .ashx (as in your suggested method above), it's an all-or-nothing scenario - meaning ALL files are processed & resolved. If I only need 1 file resolved, then I'm kinda out of luck.
posted on Friday, June 16, 2006  by vip32 @ 11:43 PM

ok that was not clear from your code or blog. i thought that it was mandatory, until i tested the code myself. cheers
posted on Thursday, June 29, 2006  by edmund @ 2:52 PM

hi,

is there a way to use the tilde within the css?

for example, i want to reference to a certain common bacground image from all available Themes, so desirebly i'll want to have a css style of:
background-image: url(~/images/bg.gif);

is there a way to do that?
edmund
posted on Thursday, June 29, 2006  by bobby @ 3:00 PM

Yes, you should be able to use tildes (~) in your css files. I use them heavily in mine.

Cheers
posted on Monday, January 22, 2007  by Martin @ 12:08 PM

Hiya,

I have a question about the following lines of code:

//Ensure ~ / in file path is resolved
if(absFilePath.IndexOf("~\\") > -1)
absFilePath = absFilePath.Replace("~", "").Replace("\\\\", "\\");


This is causing problems for us as all our websites are in a folder called "~sites~" and the tildes gets incorrectly stripped out.

Can you please elaborate on what this check is needed for?

In my testing I haven't been able to get any extraneous "~\" characters and so I'm tempted to delete those lines but would appreciate any feedback on problems this may cause.

Cheers,
posted on Monday, January 22, 2007  by Anonymous @ 12:11 PM

That line is to resolve application-level paths. If you are using absolute or relative paths, you can remove that line. The other part is to remove any double slashes that might have gotten in there.
posted on Monday, January 22, 2007  by Martin @ 12:22 PM

Thanks for the very speedy reply.

I'm unsure what you mean by the term "Application-Level" path however.

I have tried


<link runat="server" href="css/styles.css.ashx" type="text/css" rel="stylesheet">
<link runat="server" href="http://www.csharper.net/css/styles.css.ashx" type="text/css" rel="stylesheet">

and neither of them introduce any tildes that need stripping.

Could you please provide an example of the type of link that would lead to the file path ending up with surplus tildes?

Thanks in Advance.
posted on Monday, January 22, 2007  by Anonymous @ 12:23 PM

CORRECTION AS ~ / (without the space) gets converted to http://www.csharper.net !



Thanks for the very speedy reply.

I'm unsure what you mean by the term "Application-Level" path however.

I have tried


<link runat="server" href="css/styles.css.ashx" type="text/css" rel="stylesheet">
<link runat="server" href="~ /css/styles.css.ashx" type="text/css" rel="stylesheet">

and neither of them introduce any tildes that need stripping.

Could you please provide an example of the type of link that would lead to the file path ending up with surplus tildes?
posted on Monday, January 22, 2007  by Anonymous @ 12:25 PM

Application level means that it's resolved to the application - which could be the virtual directory, or the root of the site.

An example would be <link ... href="~/resources/css/mystylesheet.css.ashx" />

While typically only server-processed controls (runat="server") will resolve application paths, but this was a hack to allow it here.
posted on Monday, January 22, 2007  by Anonymous @ 12:58 PM

Thanks, got it now.

So, for example, a static HTML page could link to

<link rel="stylesheet" type="text/css" href="~ /css/styles.css.ashx">

But this would cause tildes to appear in the file path that need stripping.

In that case I'll just change the code to
Copy code to clipboard in IE or select code for Firefox
...


            if (absFilePath.Contains("\\~\\"))
            {
                absFilePath = absFilePath.Replace("\\~\\", "\\");
            }



..


Anyone see any potential problems with that?
posted on Saturday, June 30, 2007  by Lindsay @ 7:08 AM

if i'm using an HTTPModule, i can't do this, can i? I'm doing URL rewriting, and having issues with paths to find css and javascript files. The pages use a master page, which have the relative links to the css on them.. and when i point to a page in a "fake" directory, it can no longer find the css and js files. i thought i could use this as a work around, but my http handler never gets called. i'm assuming it's because i'm using an http module.

sad panda.


New Post Notification

Search Posts

Recent Posts


About Meeself
People call me Bobby DeRosa
I live somewhere in San Diego, CA
MCSD, MCAD, MCP

This theme was adapted from fUnique by fahlstad        Icons by FamFamFam        XHTML 1.0 Strict; tuned for Mozilla-powered browsers

Admin Login Administrator Login
Invalid login attempts are logged.
  Username:
  Password: