Tuesday, September 11, 2007

ASP.NET Web Application in Offline Mode Revisited

Probably most of You are aware of the app_offline.htm functionality. Basically if you put a html file named app_offline.htm in the root of a web application, the application goes to "offline" mode where all incoming requests are redirected to this file.

This seems great at first sight. You can temporarily disable the application while performing some maintenance. Specifically, you can delete all application files except the app_offline.htm and the application still works (even though the web.config is gone!) As long as the maintenance is completed you just remove or rename the app_offline.htm and the application is back from the void.

There is, however, one caveat and one drawback of this feature.

The caveat is that the app_offline.htm file must be longer that 512 bytes otherwise instead of its content you get ... 404 (can someone explain it?).

The drawback is that the offline mode affects everyone including you! Either anyone or noone can access the application. What would be nice, however, is to have the ability to temporarily redirect requests coming from outside but still be able to use the application locally.

The code below shows how to accomplish that requirement with really simple HTTP handler. To use it you have to:

  1. compile the source class
  2. add the handler section to the web.config file and put appoffline.htm (without the _ !) in the root of the application
   1: <httpHandlers>
   2:    <add verb="*" path="*.aspx" type="AppOffline.AppOfflineHandler, AppOffline" />
   3:  </httpHandlers>

 


Handler code:



   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.Web;
   5: using System.IO;
   6: using System.Web.UI;
   7:  
   8: /* Wiktor Zychla, 2007 */
   9: namespace AppOffline
  10: {
  11:     public class AppOfflineHandler : IHttpHandler
  12:     {
  13:         string appOffline;
  14:         /// <summary>
  15:         /// appoffline.htm file content as singleton
  16:         /// </summary>
  17:         public string AppOffline
  18:         {
  19:             get
  20:             {
  21:                 if ( appOffline == null )
  22:                     appOffline = OfflineFileContent;
  23:  
  24:                 return appOffline;
  25:             }
  26:         }
  27:  
  28:         /// <summary>
  29:         /// Path to the appoffline.htm file
  30:         /// </summary>
  31:         public string OfflineFilePath
  32:         {
  33:             get
  34:             {
  35:                 return context.Server.MapPath( "~/appoffline.htm" );
  36:             }
  37:         }
  38:  
  39:         /// <summary>
  40:         /// appoffline.htm file content
  41:         /// </summary>
  42:         public string OfflineFileContent
  43:         {
  44:             get
  45:             {
  46:                 return File.ReadAllText( OfflineFilePath );
  47:             }
  48:         }
  49:  
  50:         /// <summary>
  51:         /// Is the application in the offline mode?
  52:         /// 
  53:         /// Yes - if the appoffline.htm exists
  54:         /// </summary>
  55:         public bool IsOffline
  56:         {
  57:             get
  58:             {
  59:                 return File.Exists( OfflineFilePath );
  60:             }
  61:         }
  62:  
  63:         HttpContext context;
  64:  
  65:         #region IHttpHandler Members
  66:         public bool IsReusable
  67:         {
  68:             get
  69:             {
  70:                 return true;
  71:             }
  72:         }
  73:  
  74:         public void ProcessRequest( HttpContext context )
  75:         {
  76:             this.context = context;
  77:  
  78:             // offline mode and remote request?
  79:             if ( !context.Request.IsLocal &&
  80:                  IsOffline
  81:                 )
  82:             {
  83:                 context.Response.Clear();
  84:                 context.Response.Write( AppOffline );
  85:  
  86:                 context.Response.End();
  87:             }
  88:             else
  89:                 // redirect to the default processing pipe
  90:                 PageParser.GetCompiledPageInstance( 
  91:                     context.Request.Path, 
  92:                     context.Request.PhysicalPath, 
  93:                     context ).ProcessRequest( context );             
  94:         }
  95:         #endregion
  96:     }
  97: }

This approach has its own drawback - it is not "maintenance" aware in a sense that when for example some crucial modules are gone from the /bin subfolder, the application will not load at all because of non-existent modules beeing referenced from within the web.config file.


The nice thing is that you can test the application locally but all remote requests are gracefully redirected to the appoffline.htm. I suggest then to use my feature side by side with the ASP.NET app_offline.htm.

1 comment:

Bart Verkoeijen said...

Although a nice alternative, there might be a performance hit in checking a file for existence with every request. I suggest you to use a directory listener instead. You can make a static class that implements this listener and keep it alive in the app domain. You can add a property to the class that checks the state and automatically registers the listener if it doesn't exist yet (to cope with app domain flushes).

However, as you said, your alternative is not really in maintenance state. Remind that using app_ofline.htm also frees all the resources like database and file links. You probably want to have this when you upgrade important libraries...