Precompilation und die Fehlermeldung `Directory '{0}' does not exist. Failed to start monitoring file changes`

Unter zugegeben etwas besonderen Umständen kommt es beim Aufruf einer ASP.NET Seite zu dieser Fehlermeldung:

System.Web.HttpException (0x80070002): Directory ‘{0}’ does not exist. Failed to start monitoring file changes.

  • System.Web.FileChangesMonitor.FindDirectoryMonitor(String dir, Boolean addIfNotFound, Boolean throwOnError) +334
  • System.Web.FileChangesMonitor.StartMonitoringPath(String alias, FileChangeEventHandler callback, FileAttributesData& fad) +805
  • System.Web.Caching.CacheDependency.Init(Boolean isPublic, String[] filenamesArg, String[] cachekeysArg, CacheDependency dependency, DateTime utcStart) +2558
  • System.Web.Hosting.MapPathBasedVirtualPathProvider.GetCacheDependency(String virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) +334
  • System.Web.ResponseDependencyList.CreateCacheDependency(CacheDependencyType dependencyType, CacheDependency dependency) +539
  • System.Web.HttpResponse.CreateCacheDependencyForResponse(CacheDependency dependencyVary) +62
  • System.Web.Caching.OutputCacheModule.InsertResponse(HttpResponse response, HttpContext context, String keyRawResponse, HttpCachePolicySettings settings, CachedVary cachedVary, CachedRawResponse memoryRawResponse) +758
  • System.Web.Caching.OutputCacheModule.OnLeave(Object source, EventArgs eventArgs) +8782064
  • System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +68
  • System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +75

Ursache

Damit es überhaupt dazu kommen kann, müssen folgende Bedingungen gegeben sein:

  • Vorkompilierung / Precompilation
  • Webseite nicht aktualisierbar / Website not updatable
  • WebUserControl und/oder MasterPage in eigenem Verzeichnis
  • Verwendung von OutputCache

Das Problem ist, dass für MasterPages und WebUserControls keine Dummy-Datei vorgehalten werden müssen, da diese über den IIS nicht direkt aufgerufen werden. Dadurch werden aber auch Verzeichnisse, in denen sonst keine anderen Dateien liegen, nicht erstellt. Bei der Verwendung von OutputCache werden nun alle abhängigen Dateien überprüft. Dabei ist es kein Problem, wenn eine Datei fehlt - das Verzeichnis muss allerdings vorhanden sein.

Lösung

Die Lösung ist denkbar einfach. Die fehlenden Verzeichnisse müssen angelegt werden…

Innerhalb eines Web Deployment Projekts (*.wdproj) kann man dazu folgendes Target verwenden. In leicht abgewandelter Form (die Pfade und Eigenschaften müsste man anpassen) geht es so natürlich auch innerhalb eines beliebig anderen MSBuild-Projekts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Target Name="CreateEmptyDirectoriesForOutputCache"
Condition="'@(EnableUpdateable)' != 'true'">
<!--
use this target to create empty directories as a workaround for a bug in
precompilation in conjunction with @OutputCache and updatable=false
see http://gehirnwindung.de/post/2010/01/18/aspnet-precompilation-und-die-fehlermeldung-directory-does-not-exist-failed-to-start-monitoring-file-changes for more details
-->
<CreateItem Condition="'$(EnableCopyBeforeBuild)' != 'true' and '@(ExcludeFromBuild)' == ''"
Include="$(SourceWebPhysicalPath)\**\*.master;$(SourceWebPhysicalPath)\**\*.ascx"
Exclude="$(SourceWebPhysicalPath)\**\.svn\**\*.*">
<Output ItemName="EmptyFilePaths" TaskParameter="Include" />
</CreateItem>
<CreateItem Condition="'$(EnableCopyBeforeBuild)' == 'true' or '@(ExcludeFromBuild)' != ''"
Include="$(CopyBeforeBuildTargetPath)\**\*.master;$(CopyBeforeBuildTargetPath)\**\*.ascx">
<Output ItemName="EmptyFilePaths" TaskParameter="Include" />
</CreateItem>
<MakeDir Directories="$(WDTargetDir)%(EmptyFilePaths.RecursiveDir)" ContinueOnError="true" />
</Target>

Damit die Verzeichnisse angelegt werden, muss das Target auch aufgerufen werden. Z.B. mittels

1
2
3
<Target Name="AfterBuild">
<CallTarget Targets="CreateEmptyDirectoriesForOutputCache" />
</Target>

Falls <ExcludeFromBuild>-Tags enthalten sind (oder EnableCopyBeforeBuild = true ist), werden die .ascx und.master-Dateien innerhalb der Kopie (Eigenschaft CopyBeforeBuildTargetPath) gesucht. In dem Fall sollte man - falls vorhanden - mit Hilfe von <ExcludeFromBuild> z.B. Subversion-Dateien ausschließen, ansonsten werden diese Pfade auch angelegt.

Sind dagegen keine <ExcludeFromBuild>-Tags vorhanden (und die Eigenschaft EnableCopyBeforeBuild = false), dann wird das physikalische Web-Verzeichnis nach den Dateien durchsucht. Hier muss gegebenenfalls das Exclude-Attribut des ersten <CreateItem>-Tags angepasst werden. Momentan werden damit die Dateien innerhalb der .svn-Ordner ausgeblendet.

Mit Hilfe dieser Targets-Datei kann der Code einfach wiederverwendet werden. Per <Import> wird das Target innerhalb des Web Deployment Projekts geladen. Will man es für alle zukünftigen Projekte anpassen, sollte man die entsprechende Vorlage ändern (zu finden unter C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\Packages\WebDeploy.wdproj).
Alternativ dazu kann man diesen Import auch innerhalb der Datei C:\Program Files (x86)\MSBuild\Microsoft\WebDeployment\v9.0\Microsoft.WebDeployment.targets einfügen, dann gilt es für alle Projekte.

Egal wo es zum Schluß nun steht, es sieht dann ungefähr so aus (lediglich der Pfad muss angepasst werden).

1
<Import Project="./KlugeSoftware.WebDeployment.CreateEmptyDirectoriesForOutputCache.targets" />

Ein expliziter Aufruf des Targets ist hierbei nicht nötig.

Download MSBuild-Target