Calling all developers! We invite you to provide your input on Feature Experimentation by completing this brief survey.

 

Anders Hattestad
Oct 13, 2011
  6614
(4 votes)

Use client cache for resource files (css/js/images) without hassle

I usually creates a virtual path (VirtualPathNativeProvider)  that all my css, images and javascript files is placed in. One reason for this is that later the editors have access to these files with out having to access the server.

The problem comes of course when these files changes. If the current VPP folder have set

Code Snippet
  1. <staticFile expirationTime="4.0:0:0" />

the web browser will not get these new files before they force a cache reset. One way around this “problem” is to not cache it on the client. But that have some downsides.

My solution is to create my own VPP file system based on the VirtualPathNativeProvider.

The whole concept is that I remove a part of the path that contains Changed_ like this

Code Snippet
  1. Regex filter = new Regex("/Changed_[^/]*/");
  2. string FilterCacheKeyPath(string virtualPath)
  3. {
  4.     virtualPath = filter.Replace(virtualPath, "/");
  5.     return virtualPath;
  6. }

Then I replace all the methods to use the striped version of the path/filename

Code Snippet
  1. public class FilSystemWithCacheKey : EPiServer.Web.Hosting.VirtualPathNativeProvider
  2. {
  3.     public FilSystemWithCacheKey(string name, NameValueCollection configParameters)
  4.         : base(name, configParameters)
  5.     {
  6.     }
  7.  
  8.     public override bool FileExists(string virtualPath)
  9.     {
  10.         return base.FileExists(FilterCacheKeyPath(virtualPath));
  11.     }
  12.     public override bool DirectoryExists(string virtualPath)
  13.     {
  14.         return base.DirectoryExists(FilterCacheKeyPath(virtualPath));
  15.     }
  16.     public override System.Web.Hosting.VirtualFile GetFile(string virtualPath)
  17.     {
  18.         return base.GetFile(FilterCacheKeyPath(virtualPath));
  19.     }
  20.     public override System.Web.Hosting.VirtualDirectory GetDirectory(string virtualPath)
  21.     {
  22.         return base.GetDirectory(FilterCacheKeyPath(virtualPath));
  23.     }
  24.     public override EPiServer.Security.AccessLevel QueryAccess(string virtualPath, System.Security.Principal.IPrincipal user)
  25.     {
  26.         return base.QueryAccess(FilterCacheKeyPath(virtualPath), user);
  27.     }
  28.     Regex filter = new Regex("/Changed_[^/]*/");
  29.     string FilterCacheKeyPath(string virtualPath)
  30.     {
  31.         virtualPath = filter.Replace(virtualPath, "/");
  32.         return virtualPath;
  33.     }

Then the fun begins Smile

I created a method that have path and filename as input

  1. %=FilSystemWithCacheKey.GetCachePath("/Framework/","styles/screen.css") %>

That will return a path with the last changed inside the first path.

image

Code Snippet
  1. public static string GetCachePath(string path, string filename)
  2. {
  3.     var newUrl = HttpContext.Current.Cache["VersionPath_" + path + filename] as string;
  4.     if (newUrl == null)
  5.     {
  6.         VirtualPathHandler instance = VirtualPathHandler.Instance;
  7.         var dir = instance.GetDirectory(path, false) as NativeDirectory;
  8.         var dirs = new List<string>();
  9.         dirs.Add((dir as NativeDirectory).LocalPath);
  10.         newUrl = path + "Changed_" + LastAccessed((dir as NativeDirectory).LocalPath, dirs) + "/" + filename;
  11.         HttpContext.Current.Cache.Insert("VersionPath_" + path + filename, newUrl, new CacheDependency(dirs.ToArray()));
  12.     }
  13.     return newUrl;
  14. }
  15. public static string LastAccessed(string path, List<string> paths)
  16. {
  17.     Stack<DirectoryInfo> dirs = new Stack<DirectoryInfo>();
  18.     FileInfo mostRecent = null;
  19.  
  20.     dirs.Push(new DirectoryInfo(path));
  21.  
  22.     while (dirs.Count > 0)
  23.     {
  24.         DirectoryInfo current = dirs.Pop();
  25.  
  26.         Array.ForEach(current.GetFiles(), delegate(FileInfo f)
  27.         {
  28.             if (mostRecent == null || mostRecent.LastWriteTime < f.LastWriteTime)
  29.                 mostRecent = f;
  30.         });
  31.  
  32.         Array.ForEach(current.GetDirectories(), delegate(DirectoryInfo d)
  33.         {
  34.             paths.Add(d.FullName);
  35.             dirs.Push(d);
  36.         });
  37.     }
  38.     return mostRecent.LastWriteTime.ToString("ddMMyyyy_HHmmss"); ;
  39. }

I add this to the Cache and add dependencies to all sub directories to the path.

since all my css links to the images relative to the path all changed to the /Framework/ files will force a new path and therefore a new client cache.

Oct 13, 2011

Comments

Joel Abrahamsson
Joel Abrahamsson Oct 13, 2011 03:59 PM

Nice post Anders! I like the approach and think I'll try it out in my next project.

One question though, I guess this means you don't put this files under source control? Or do you do that and instruct the customers to let you know if they do change something? In other words: what's the work flow?

Anders Hattestad
Anders Hattestad Oct 13, 2011 04:03 PM

The Framework VPP folder is in source control. And the client knows that they shold not change there. But 1-2 years from now they maybe want to change a logo or something and they have the possiblity :)

Oct 13, 2011 05:01 PM

What about just adding a native vpp pointing to your styles folder and add the css files to your markup with SquishIt or what ever?
Wouldn't that give you the same result but with the option to have SquishIt minimize the files too?

Anders Hattestad
Anders Hattestad Oct 13, 2011 06:48 PM

If you then change a image the client cache will not be reset

Nebud Kadnezzar
Nebud Kadnezzar Oct 13, 2011 11:44 PM

Why o why the use of Array.Foreach(bla bla)????
Why not simply use a foreach(...) ???

Compare:
Array.ForEach(current.GetFiles(), delegate(FileInfo f)
{
if (mostRecent == null || mostRecent.LastWriteTime < f.LastWriteTime)
mostRecent = f;
});

With:
foreach(FileInfo f in current.GetFiles())
{
if (mostRecent == null || mostRecent.LastWriteTime < f.LastWriteTime)
mostRecent = f;
}

It seems as if everyone just needs to show off their C#-fu in every possible way, the one who used the most anonymous methods and lambdaexpressions at the end of the day wins...

Anders Hattestad
Anders Hattestad Oct 14, 2011 06:23 AM

Lol. Mr Google wrote that method:)

Oct 14, 2011 09:44 AM

Anders Hattestad > If you then change a image the client cache will not be reset

You're right. In the back of my head I thought that maybe I just add a new image and change the css to point to the new file, but modifying the image would not work well with my solution.

Anders Hattestad
Anders Hattestad Oct 14, 2011 10:27 AM

But you are right that you could SquishIt together and add the Changed part to the url beeing returned, but since the client is only gonna get these files one time I thought the overhead was not necessery.

Magnus Rahl
Magnus Rahl Nov 11, 2011 08:34 AM

I use a similar approach but vary a querystring argument rather than a part of the path. I have heard some whispers that different querystring is not treated the same as different path by the some browsers. It seems to work, but I'm happy to hear any arguments against the querystring approach?

Anders Hattestad
Anders Hattestad Nov 11, 2011 01:00 PM

What do you do with images refered by the CSS file? Querystring there also?

Magnus Rahl
Magnus Rahl Nov 11, 2011 04:16 PM

Good point. But anything about the querystring vs browser caching? I just learned that IIS kernel mode caching simply skips if there's a querystring present, so there's one reason against it (worse performance when the file is actually retieved). But VPP:s can't be kernel mode cached so there's no difference there?

Anders Hattestad
Anders Hattestad Nov 12, 2011 02:15 AM

I think that the browser do a check (head request) for a resource when there is a querystring parameter

Please login to comment.
Latest blogs
Optimizely CMS easy RSS feed integration library

As I've mentioned in my  previous blog post , while I was developing the Optimizely version of my blog, I tried to look for a library that could...

David Drouin-Prince | Jan 25, 2025 | Syndicated blog

Decimal numbers in Optimizely Graph

Storing prices as decimal numbers on a commerce website and planning to expose them through Optimizely Graph? It might not be as straightforward as...

Damian Smutek | Jan 23, 2025 | Syndicated blog

Find and delete non used media and blocks

On my new quest to play around with Blazor and MudBlazor I'm going back memory lane and porting some previously plugins. So this time up is my plug...

Per Nergård (MVP) | Jan 21, 2025

Optimizely Content Graph on mobile application

CG everywhere! I pull schema from our default index https://cg.optimizely.com/app/graphiql?auth=eBrGunULiC5TziTCtiOLEmov2LijBf30obh0KmhcBlyTktGZ in...

Cuong Nguyen Dinh | Jan 20, 2025

Image Analyzer with AI Assistant for Optimizely

The Smart Image Analyzer is a new feature in the Epicweb AI Assistant for Optimizely CMS that automates the management of image metadata, such as...

Luc Gosso (MVP) | Jan 16, 2025 | Syndicated blog

How to: create Decimal metafield with custom precision

If you are using catalog system, the way of creating metafields are easy – in fact, you can forget about “metafields”, all you should be using is t...

Quan Mai | Jan 16, 2025 | Syndicated blog