I wanted to write this post to describe how to fix a problem that I don't think I'm alone in experiencing. When working in a development environment EPiServer can appear to serve cached mark up even though the file has been changed or even the solution has been rebuilt. Only an iisreset appears to force a refresh.

Colleagues have certainly experienced it and it's also been asked about in the forums too. Yet no one seems to have got to the bottom of it. The problem looks a little something like this:

  • You are running EPiServer on your Windows 7/Vista development machine
  • You are using IIS in integrated mode to host the development site
  • You have an EPiServer web application
  • You make a change to an .aspx or .master file in your solution
  • No need to rebuild since it's just a mark up change and IIS will pick up the change - right?
  • You go to your browser and refresh
  • Nothing appears to have changed in the template
  • Ah, you didn't hit save in Visual Studio - right?
  • You switch back to Visual Studio and save again
  • You hit refresh again
  • Still looks the same
  • You think, "ah must be the browser cache"
  • So you hit Ctrl+F5 (or use incognito mode in Chrome)
  • Still no change
  • You rebuild the application
  • Still no change
  • You hit your mouse on the desk
  • Still no change - but at least you feel a little better

By this point most developers end up resorting to an iisreset but this is not only time consuming but frustrating too.

So I decided to investigate and address it. We know that ASP.net keeps a watch on the special folders such as the \bin folder and recycles if anything has changed as it could affect the running application. The watching of these “special” folders is managed by the FileChangesMonitor class. Unhelpfully this class Isn't exposed by the HttpRuntime (and neither is the HttpRuntime instance for that matter). Even less helpfully Microsoft has refused to expose the FileChangesMonitor object using public properties (Microsoft Connect link). Therefore it's reflection to the rescue.

By adding the following class to your solution (works in EPiServer 5 or 6) we add other folders to the list of folders being watched and force ASP.net to recycle if anything changes in them:

#if DEBUG
    public class ForceRefresh : PlugInAttribute
    {
        public static bool addedExtraFolders = false;
 
        public static void Start()
        {
            if (addedExtraFolders == false)
            {
                monitorAllFileChanges();
                addedExtraFolders = true;
            }
        }
 
        private static void monitorAllFileChanges()
        {
            try
            {
                //Get a handle to the FileChangesMonitor (please Microsoft make it public to us!)
                System.Reflection.PropertyInfo p = typeof(System.Web.HttpRuntime).GetProperty("FileChangesMonitor", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
                object o = p.GetValue(null, null);
 
                //Get the monitored directory list and update the value to include the templates folder
                System.Reflection.FieldInfo f = o.GetType().GetField("s_dirsToMonitor", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Static);
                string[] monitor = (string[])f.GetValue(o);
                List<string> monitorList = monitor.ToList();
                monitorList.Add("Templates"); // < add your template folders here
                monitor = monitorList.ToArray();
                f.SetValue(o.GetType(), monitor);
 
                //Reinvoke the monitoring method to watch the new folder structure
                System.Reflection.MethodInfo m = typeof(System.Web.HttpRuntime).GetMethod("StartMonitoringDirectoryRenamesAndBinDirectory", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
                System.Reflection.FieldInfo rt = typeof(System.Web.HttpRuntime).GetField("_theRuntime", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
                m.Invoke(rt.GetValue(null), new object[] { });
            }
            catch { } //Tut tut but this is for development after all
        }
    }
#endif

I’ve included some conditional compilation to ensure this never works on anything outside of development/test machines.

Conclusion

I'd be interested to hear if others have experienced this problem. In particular if they've found any other ways around this (since I'm not overly happy about using reflection to modify relatively low level objects).

Maybe it's something EPiServer could look into and bake into the next release (OK it may be a little late to make it into R2!).

Disclaimer

This tool is only designed for development environments so I'd recommend building the explicit removal if it into your build scripts or separating into a separate assembly and removing as part of build. Use it outside of a dev environment at your own risk.


Comments