Taming the BuildManager, ASP.Net Temp files and AppDomain restarts
I’ve recently had to do a bunch of research in to the BuildManager and how it deals with caching assemblies since my involvement with creating an ASP.Net plugin engine. Many times people will attempt to restart their AppDomain by ‘bumping’ their web.config files, meaning adding a space character or carriage return and then saving it. Sometimes you may have noticed that this does not always restart your AppDomain the way you’d expect and some of the new types you’d expect to have loaded in your AppDomain simply aren’t there. This situation has cropped up a few times when using the plugin engine that we have built in to Umbraco v5 and some people have resorted to manually clearing out their ASP.Net temp files and then forcing an IIS Restart, but this is hardly ideal. The good news is that we do have control over how these assemblies get refreshed, in fact the BuildManager reloads/refreshes/clears assemblies in different ways depending on how the AppDomain is restarted.
The hash.web file
An important step during the BuildManager initialization phase is a method call to BuildManager.CheckTopLevelFilesUpToDate which does some interesting things. First it checks if a special hash value is on disk and is not zero. You may have noticed a file in your ASP.Net temp files: /hash/hash.web and it will contain a value such as: 4f34227133de5346. This value represents a unique hash of many different combined objects that the BuildManager monitors. If this file exists and the value can be parsed properly then the BuildManager will call: diskCache.RemoveOldTempFiles(); What this does is the following:
- removes the CodeGen temp resource folder
- removes temp files that have been saved during CodeGen such as *.cs
- removes the special .delete files and their associated original files which have been created when shutting down the app pool and when the .dll files cannot be removed (due to file locking)
Creating the hash value
The next step is to create this hash value based on the current objects in the AppDomain. This is done using an internal utility class in the .Net Framework called: HashCodeCombiner (pretty much everything that the BuildManager references is marked Internal! ). The process combines the following object values to create the hash (I’ve added in parenthesis the actual properties the BuildManager references):
- the app's physical path, in case it changes (HttpRuntime.AppDomainAppPathInternal)
- System.Web.dll (typeof(HttpRuntime).Module.FullyQualifiedName)
- machine.config file name (HttpConfigurationSystem.MachineConfigurationFilePath)
- root web.config file name, please note that this is not your web apps web.config (HttpConfigurationSystem.RootWebConfigurationFilePath)
- the hash of the <compilation> section (compConfig.RecompilationHash)
- the hash of the system.web/profile section (profileSection.RecompilationHash)
- the file encoding set in config (appConfig.Globalization.FileEncoding)
- the <trust> config section (appConfig.Trust.Level & appConfig.Trust.OriginUrl)
- whether profile is enabled (ProfileManager.Enabled)
- whether we are precompiling with debug info (but who precompiles :) (PrecompilingWithDebugInfo)
Then we do a check for something I didn’t know existed called Optimize Compilations which will not actual affect the hash file value for the following if it is set to true (by default is is false):
- the ‘bin’ folder (HttpRuntime.BinDirectoryInternal)
- App_WebReferences (HttpRuntime.WebRefDirectoryVirtualPath)
- App_GlobalResources (HttpRuntime.ResourcesDirectoryVirtualPath)
- App_Code (HttpRuntime.CodeDirectoryVirtualPath)
- Global.asax (GlobalAsaxVirtualPath)
Refreshing the ASP.Net temp files (CodeGen files)
The last step of this process is to check if the persisted hash value in the hash.web file equals the generated hash value from the above process. If they do not match then a call is made to diskCache.RemoveAllCodegenFiles(); which will:
- clear all codegen files, removes all files in folders but not the folders themselves,
- removes all root level files except for temp files that are generated such as .cs files, etc...
This essentially clears your ASP.Net temp files completely, including the MVC controller cache file, etc…
Then the BuildManager simply resaves this calculated has back to the hash.web file.
What is the best way to restart your AppDomain?
There is really no ‘best’ way, it just depends on your needs. If you simply want to restart your AppDomain and not have the overhead of having your app recompile all of your views and other CodeGen classes then it’s best that you simply ‘bump’ your web.config by just adding a space, carriage return or whatever. As you can see above the hash value is not dependant on your local web.config file’s definition (timestamp, etc…). However, the hash value is dependent on some other stuff in your apps configuration such as the <compilation> section, system.web/profile section, the file encoding configured, and the <trust> section. If you update any value in any of these sections in your web.config it will force the BuildManager to clear all cached CodeGen files which is the same as clearing your ASP.Net temp files.
So long as you don’t have optimizeCompilations set to true, then the easiest way to clear your CodeGen files is to simply add a space or carriage return to your global.asax file or modify something in the 4 other places that the BuildManager checks locally: App_Code, App_GlobalResources, App_WebResources, or modify/add/remove a file in your bin folder.
How does this affect the ASP.Net plugin engine?
Firstly, i need to update that post as the code in the plugin engine has changed quite a bit but you can find the latest in the Umbraco source code on CodePlex. With the above knowledge its easy to clear out any stale plugins by perhaps bumping your global.asax file, however this is still not an ideal process. With the research I’ve done in to the BuildManager I’ve borrowed some concepts from it and learned of a special “.delete” file extension that the BuildManager looks for during AppDomain restarts. With this new info, I’ve already committed some new changes to the PluginManager so that you shouldn’t need to worry about stale plugin DLLs.