ASP.NET File Change Notifications, exactly which files and directories are monitored?

Perhaps one of the most loved and hated features of ASP.NET is the ability to detect file changes and automatically recompile or reconfigure the application.  In development scenarios it’s very nice because you can access the application in a browser at the same time you’re modifying ASPX, ASAX, DLL, CONFIG, and other application files, and you don’t have to stop the web server process in order to update DLLs or configuration.  The changes are picked up immediately, but in order to make this possible we have to recycle the AppDomain.  In deployment scenarios, this feature can be a problem.  There are many files and folders (e.g. global.asax, web.config, BIN, App_Code, etc) that trigger an AppDomain unload when changed.  A subsequent request to the application will cause the AppDomain to be loaded again and the new files will be compiled, if necessary.  Restarting the application is expensive due to the disk access and recompilation of files.  Many applications also have expensive initialization steps that occur when the AppDomain is loaded, such as accessing SQL to populate local stores with frequently accessed data.  So what exactly does ASP.NET monitor and how?

ASP.NET uses the Win32 function ReadDirectoryChangesW to monitor directories and files.  Given a directory handle, this function will let you know lots of information about changes to the directory or the files and directories contained within it.  By passing different flags, you tell it exactly what information you’re interested in and it informs you when there are changes.  ASP.NET monitors directories with the following flags (they are fairly self-explanatory, but you can refer to ReadDirectoryChangesW documentation for details):

FILE_NOTIFY_CHANGE_FILE_NAME
FILE_NOTIFY_CHANGE_DIR_NAME
FILE_NOTIFY_CHANGE_CREATION
FILE_NOTIFY_CHANGE_SIZE
FILE_NOTIFY_CHANGE_LAST_WRITE
FILE_NOTIFY_CHANGE_SECURITY

The above flags are the ones that ASP.NET uses, but not all of them are used for each directory handle that is monitored.  So which directories are monitored?  By default in ASP.NET 2.0, there are six directory monitors for each application path, and two additional directory monitors for each virtual subdirectory path.  This is best explained with an example.  Suppose that we have an application with a virtual path of "/vpath" and a physical path of "c:\ppath".  If you make a request to a file located at "/vpath/f.aspx", the following directory monitors will be created: 

#1) The application physical root path and all of its subdirectories are monitored for subdirectory name changes or deletions.  To be more precise, if FILE_NOTIFY_INFORMATION.Action is FILE_ACTION_RENAMED_OLD_NAME or FILE_ACTION_REMOVED, the AppDomain is unloaded.  This means that if you rename or delete a subdirectory beneath the application physical root, the application will be restarted.  This is discussed in Todd Carter's blog post.

ReadDirectoryChangesW is called with these arguments:

Directory= "c:\ppath"
WatchSubtree= True
NotifyFilter= FILE_NOTIFY_CHANGE_DIR_NAME

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.StartMonitoringDirectoryRenamesAndBinDirectory(String dir, FileChangeEventHandler callback)
   at System.Web.HttpRuntime.HostingInit(HostingEnvironmentFlags hostingFlags)

#2)  The "bin", "App_Code", "App_WebReferences", "App_GlobalResources", and "App_Browsers" subdirectories of the application root folder are monitored for creation, deletion, renaming, ACL changes, changes to the last-write time, and changes to the size.  If any of these things change, the AppDomain is unloaded.  Note that one directory monitor is created to monitor all five of these subfolders when they do not exist so that ASP.NET can catch their creation.  If they do exist, then a unique directory monitor is created for that subdirectory.  So if "bin" exists and the other four do not exist, you will have a total of two directory monitors, one for "bin" and one that monitors for the creation of the other four.  Or if "bin" and "App_Code" exist but the others do not, you will have three directory monitors, one for "bin", one for "App_Code", and one that monitors for the creation of the other three.

ReadDirectoryChangesW is called with these arguments:

Directory= "c:\ppath"
WatchSubtree= False
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.ListenToSubdirectoryChanges(String dirRoot, String dirToListenTo)
   at System.Web.FileChangesMonitor.StartMonitoringDirectoryRenamesAndBinDirectory(String dir, FileChangeEventHandler callback)
   at System.Web.HttpRuntime.HostingInit(HostingEnvironmentFlags hostingFlags)

#3) The machine.config and root web.config file are monitored for changes.  A change to either of these files will unload the AppDomain.

ReadDirectoryChangesW is called with these arguments:

Directory= "%WINDIR%\Microsoft.NET\Framework\v2.0.50727\Config"
WatchSubtree= FALSE
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.StartMonitoringFile(String alias, FileChangeEventHandler callback)
   at System.Web.Configuration.WebConfigurationHost.StartMonitoringStreamForChanges(String streamName, StreamChangeCallback callback)
   at System.Configuration.BaseConfigurationRecord.MonitorStream(String configKey, String configSource, String streamname)
   at System.Configuration.BaseConfigurationRecord.InitConfigFromFile()
   at System.Configuration.BaseConfigurationRecord.Init(IInternalConfigRoot configRoot, BaseConfigurationRecord parent, String configPath, String locationSubPath)
   at System.Configuration.RuntimeConfigurationRecord.Create(InternalConfigRoot configRoot, IInternalConfigRecord parent, String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetUniqueConfigRecord(String configPath)
   at System.Web.Configuration.HttpConfigurationSystem.GetUniqueConfigRecord(String configPath)
   at System.Web.CachedPathData.Init(CachedPathData parentData)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.Configuration.RuntimeConfig.GetAppLKGConfig()
   at System.Web.HttpRuntime.GetInitConfigSections
   at System.Web.HttpRuntime.HostingInit(HostingEnvironmentFlags hostingFlags)

#4)  The web.config of parent applications are monitored.  In this example, the root application "/" is the only parent.  A change to this file will unload the AppDomain.

ReadDirectoryChangesW is called with these arguments:

Directory= "c:\inetpub\wwwroot"
WatchSubtree= False
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:
  

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.StartMonitoringFile(String alias, FileChangeEventHandler callback)
   at System.Web.Configuration.WebConfigurationHost.StartMonitoringStreamForChanges(String streamName, StreamChangeCallback callback)
   at System.Configuration.BaseConfigurationRecord.MonitorStream(String configKey, String configSource, String streamname)
   at System.Configuration.BaseConfigurationRecord.InitConfigFromFile()
   at System.Configuration.BaseConfigurationRecord.Init(IInternalConfigRoot configRoot, BaseConfigurationRecord parent, String configPath, String locationSubPath)
   at System.Configuration.RuntimeConfigurationRecord.Create(InternalConfigRoot configRoot, IInternalConfigRecord parent, String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetUniqueConfigRecord(String configPath)
   at System.Web.Configuration.HttpConfigurationSystem.GetUniqueConfigRecord(String configPath)
   at System.Web.CachedPathData.Init(CachedPathData parentData)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.Configuration.RuntimeConfig.GetAppLKGConfig()
   at System.Web.HttpRuntime.GetInitConfigSections
   at System.Web.HttpRuntime.HostingInit(HostingEnvironmentFlags hostingFlags)

#5)   The web.config in the application root is monitored.  A change to this file will unload the AppDomain.

ReadDirectoryChangesW is called with these arguments:

Directory= "c:\ppath"
WatchSubtree= False
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.StartMonitoringFile(String alias, FileChangeEventHandler callback)
   at System.Web.Configuration.WebConfigurationHost.StartMonitoringStreamForChanges(String streamName, StreamChangeCallback callback)
   at System.Configuration.BaseConfigurationRecord.MonitorStream(String configKey, String configSource, String streamname)
   at System.Configuration.BaseConfigurationRecord.InitConfigFromFile()
   at System.Configuration.BaseConfigurationRecord.Init(IInternalConfigRoot configRoot, BaseConfigurationRecord parent, String configPath, String locationSubPath)
   at System.Configuration.RuntimeConfigurationRecord.Create(InternalConfigRoot configRoot, IInternalConfigRecord parent, String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetUniqueConfigRecord(String configPath)
   at System.Web.Configuration.HttpConfigurationSystem.GetUniqueConfigRecord(String configPath)
   at System.Web.CachedPathData.Init(CachedPathData parentData)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.Configuration.RuntimeConfig.GetAppLKGConfig()
   at System.Web.HttpRuntime.GetInitConfigSections
   at System.Web.HttpRuntime.HostingInit(HostingEnvironmentFlags hostingFlags) 

#6)  The hash.web file located in the "hash" subdirectory of the Temporary ASP.NET Files folder is monitored.  This monitor was added to support development scenarios, in which both the ClientBuildManager and the runtime BuildManager are updating the application files.  Basically, since there are two build managers, there was a need to keep them in sync, and the hash.web file is used for that purpose.

ReadDirectoryChangesW is called with these arguments:

Directory= "%WINDIR%\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\repro\19a9eafa\c159e372\hash"
WatchSubtree= False
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.StartMonitoringFile(String alias, FileChangeEventHandler callback)
   at System.Web.Compilation.BuildManager.CheckTopLevelFilesUpToDate2(StandardDiskBuildResultCache diskCache)
   at System.Web.Compilation.BuildManager.CheckTopLevelFilesUpToDate(StandardDiskBuildResultCache diskCache)
   at System.Web.Compilation.BuildManager.RegularAppRuntimeModeInitialize()
   at System.Web.Compilation.BuildManager.Initialize()
   at System.Web.Compilation.BuildManager.InitializeBuildManager()
   at System.Web.HttpRuntime.HostingInit(HostingEnvironmentFlags hostingFlags)

 

As mentioned earlier, the six directory monitors mentioned above are created when a request is made to "/vpath/f.aspx".  If there is a subdirectory and a request is made to "/vpath/subdir/f.aspx", then two additional directory monitors will be created:  

#7)  The web.config in any subdirectory of the application is monitored.  A change to this file will unload the AppDomain.

ReadDirectoryChangesW is called with these arguments:

Directory= "c:\ppath\subdir"
WatchSubtree= False
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.StartMonitoringFile(String alias, FileChangeEventHandler callback)
   at System.Web.Configuration.WebConfigurationHost.StartMonitoringStreamForChanges(String streamName, StreamChangeCallback callback)
   at System.Configuration.BaseConfigurationRecord.MonitorStream(String configKey, String configSource, String streamname)
   at System.Configuration.BaseConfigurationRecord.InitConfigFromFile()
   at System.Configuration.BaseConfigurationRecord.Init(IInternalConfigRoot configRoot, BaseConfigurationRecord parent, String configPath, String locationSubPath)
   at System.Configuration.RuntimeConfigurationRecord.Create(InternalConfigRoot configRoot, IInternalConfigRecord parent, String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetConfigRecord(String configPath)
   at System.Configuration.Internal.InternalConfigRoot.GetUniqueConfigRecord(String configPath)
   at System.Web.Configuration.HttpConfigurationSystem.GetUniqueConfigRecord(String configPath)
   at System.Web.CachedPathData.Init(CachedPathData parentData)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.CachedPathData.GetConfigPathData(String configPath)
   at System.Web.CachedPathData.GetVirtualPathData(VirtualPath virtualPath, Boolean permitPathsOutsideApp)
   at System.Web.HttpContext.GetFilePathData()
   at System.Web.HttpContext.GetConfigurationPathData()
   at System.Web.ClientImpersonationContext.Start(HttpContext context, Boolean throwOnError)
   at System.Web.HttpApplication.OnThreadEnter()

#8)  The "App_LocalResources" subdirectory of each virtual subdirectory is monitored for creation, deletion, renaming, ACL changes, changes to the last-write time, and changes to the size.  If any of these things change, the AppDomain is unloaded.  This monitor is created on the virtual directory itself if the "App_LocalResources" folder does not exist; otherwise, the monitor is created on the "App_LocalResources" directory.

ReadDirectoryChangesW is called with these arguments:

Directory= "c:\ppath\subdir"
WatchSubtree= False
NotifyFilter= FILE_NOTIFY_CHANGE_FILE_NAME
                  | FILE_NOTIFY_CHANGE_DIR_NAME
                  | FILE_NOTIFY_CHANGE_CREATION
                  | FILE_NOTIFY_CHANGE_SIZE
                  | FILE_NOTIFY_CHANGE_LAST_WRITE
                  | FILE_NOTIFY_CHANGE_SECURITY

The stack trace for the call is:

   at System.Web.DirMonCompletion..ctor(DirectoryMonitor dirMon, String dir, Boolean watchSubtree, UInt32 notifyFilter)
   at System.Web.DirectoryMonitor.StartMonitoring()
   at System.Web.DirectoryMonitor.StartMonitoringFile(String file, FileChangeEventHandler callback, String alias)
   at System.Web.FileChangesMonitor.ListenToSubdirectoryChanges(String dirRoot, String dirToListenTo)
   at System.Web.FileChangesMonitor.StartListeningToVirtualSubdirectory(VirtualPath virtualDir)
   at System.Web.Compilation.BuildManager.EnsureFirstTimeDirectoryInit(VirtualPath virtualDir)
   at System.Web.Compilation.BuildManager.GetBuildResultFromCacheInternal(String cacheKey, Boolean keyFromVPP, VirtualPath virtualPath, Int64 hashCode)
   at System.Web.Compilation.BuildManager.GetVPathBuildResultFromCacheInternal(VirtualPath virtualPath)
   at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal
   at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert
   at System.Web.Compilation.BuildManager.GetVirtualPathObjectFactory(VirtualPath virtualPath, HttpContext context, Boolean allowCrossApp, Boolean noAssert)
   at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath
   at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)
   at System.Web.UI.PageHandlerFactory.System.Web.IHttpHandlerFactory2.GetHandler
   at System.Web.HttpApplication.MapHttpHandler(HttpContext context, String requestType, VirtualPath path, String pathTranslated, Boolean useAppConfig)
   at System.Web.HttpApplication.MapHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) 

So what about global.asax, or the f.aspx page that we used in the example?  Aren't they monitored?  Yes, they are, but they piggy back on one of the directory monitors listed above.  In the example, both global.asax and f.aspx would be added to the directory monitor described in #5.  Each directory monitor is capable of monitoring many different files, each with a unique callback for notification upon any changes.  I hope this helps your understanding. I've also included an ASPX page code sample below that will display the number of requests, number of AppDomain restarts, and number of active directory monitors.  I'm not sure why you would want to know how many directory monitors are active in your application, but if someone asks you, you can always requests this page and answer them.  :)

 

 <%@ import namespace="System.Diagnostics"%>
<%@ import namespace="System.Reflection"%>
 <script runat="server" language="c#">
void Page_Load() {
    PerformanceCounter pc1 = new PerformanceCounter("ASP.NET Applications", 
                                                    "Requests Total", 
                                                    "__Total__");
    
    PerformanceCounter pc2 = new PerformanceCounter("ASP.NET", 
                                                    "Application Restarts");
    
    Type t = typeof(HttpRuntime).Assembly.GetType(
                                               "System.Web.DirMonCompletion");
    
    int dirMonCount = (int) t.InvokeMember("_activeDirMonCompletions",
                                           BindingFlags.NonPublic 
                                           | BindingFlags.Static
                                           | BindingFlags.GetField, 
                                           null, 
                                           null, 
                                           null); 
      
    // The perf client polls the server every 400 milliseconds
    System.Threading.Thread.Sleep(800);
    
    Response.Output.WriteLine(
                            "Requests={0},Restarts={1},DirMonCompletions={2}",
                            pc1.NextValue(), 
                            pc2.NextValue(), 
                            dirMonCount);
}
</script>

 

 -Thomas