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

by wolfgang@gehirnwindung.de (Wolfgang) Januar 18, 2010 00:26

Unter 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 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.

<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.aspx 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

<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).

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

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

Download (1,16 kb)

Tags: , ,

ASP.NET

URL Rewrite und der SiteMap-Provider (Request.PathInfo)

by Wolfgang Kluge Mai 13, 2009 20:03

Heute wollte ich einer meiner Seiten so umstellen, dass URL-Rewrite verwendet wird. Das ging auch recht schnell (entsprechende Links gesetzt und über Request.PathInfo die Parameter ausgelesen), allerdings stellte sich beim Testen dann heraus, dass ich ein Problem mit dem Menü hatte. Dieses konnte nun nicht mehr die aktuelle Seite markieren... Das Menü wird über eine Sitemap erstellt.

Verkürzt sieht der Code für das Menü so aus:

<asp:SiteMapDataSource ID="MenuSource" runat="server" SiteMapProvider="menu" />
<asp:Menu ID="mnu" runat="server" DataSourceID="MenuSource">

In meiner Sitemap sind die entsprechenden URLs eingetragen. Z.B.

<siteMapNode title="Startseite" url="~/default.aspx" />
<siteMapNode title="Projekte" url="~/projects.aspx"/>
<siteMapNode title="Eigene Projekte" url="~/projects.aspx/own"/>

Wird die Seite "/projects.aspx" aufgerufen, so wird der Eintrag "Projekte" entsprechend gekennzeichnet (mittels CSS-Class "AspNet-Menu-Selected"). Das hat auch noch mit "/projects.aspx?test=1" wunderbar funktioniert. Wird dagegen die Seite "/projects.aspx/test" aufgerufen, wird der entsprechende Eintrag nicht mehr gefunden, da mit Request.RawURL verglichen wird. Und da steht "/projects/test" drin.

Es ist also nur verständlich, dass der Provider den Eintrag nicht findet und dementsprechend kein Menüeintrag als aktiv gekennzeichnet werden kann. Erschwerend kommt hinzu, dass durchaus auch mal ein Treffer dabei sein kann (wie in diesem Beispiel beim Aufruf von "/projects.aspx/own").

Da hilft nur selbst den richtigen Eintrag suchen. Das ist natürlich nur nötig, wenn Request.PathInfo gefüllt ist.

protected override void OnInit( EventArgs e ) {
    base.OnInit( e );
    if( MenuSource.Provider.CurrentNode == null && !string.IsNullOrEmpty( Request.PathInfo ) ) {
        MenuSource.Provider.SiteMapResolve += new SiteMapResolveEventHandler( SiteMap_SiteMapResolve );
    }
}

private SiteMapNode SiteMap_SiteMapResolve( object sender, SiteMapResolveEventArgs e ) {
    SiteMapProvider provider = sender as SiteMapProvider;
    if( provider == null ) return null;
    return provider.FindSiteMapNode( e.Context.Request.AppRelativeCurrentExecutionFilePath );
}

Sobald MenuSource.Provider.CurrentNode == null ist und in Request.PathInfo etwas steht, wird auf das Ereignis SiteMapResolve des Providers reagiert. Dort wird nach einem Eintrag im aktuellen SiteMapProvider gesucht, der nur dem Pfad (ohne PathInfo-Angaben) entspricht.

Wenn man das Menü innerhalb eines Controls hat (wo es hingehört *g*), muss der Code auch nur 1x angegeben werden.

Tags: , ,

ASP.NET

Powered by BlogEngine.NET 1.6.1.6
Theme by Mads Kristensen | Modified by Mooglegiant and me ;)