If you’ve developed on a web application of any degree of complexity, you’ve encountered times when you would like to politely take the site offline or make it unavailable to some users. ASP.NET 2.0 introduced a nifty new feature the App_Offline.htm file. If ASP.NET detects this file in your vroot, it drains the request queue, displays the contents of App_Offline.htm for any new request, and politely shuts down the app domain. You can then update the application, remove (or rename) the App_Offline.htm file and a new app domain will be spun up to start servicing requests again. Wonderful stuff, but what happens when your requirements are more complex? For instance, what if you need to take the application offline for a nightly database
backup? What if certain users can only access a data entry application
during certain hours of the day? What if you want to deploy updates to
an application and only allow a group of testers and/or admins in to
verify the updates? Enter the SiteAvailabilityModule. The SiteAvailabilityModule allows you to control whether or not your site is available to users based on group membership and time of day. For instance, normal users are only allowed access during normal business hours, but administrators can access the site at any time for maintenance and troubleshooting.

Now before we get too deeply engrossed in the code, let’s take a step back and think about how one might naively implement this feature. If you don’t know your ASP.NET pipeline, Grasshopper, you would likely code this logic into every Page_Load method on every ASPX page. If you were smart (but still didn’t know your ASP.NET pipeline), you would write a helper class so that the logic isn’t repeated in every Page_Load. (Neither solution is ideal because it’s too easy to forget to add the logic when implementing a new page.) If you were really smart, you would know a thing or two about the ASP.NET pipeline and realize that you could implement a HttpModule to accomplish this in one place.

Let’s start by taking a look at the ASP.NET pipeline:

The stages in the above diagram marked with an asterisk (*) have a corresponding “Post” event. For example, AuthenticateRequest and PostAuthenticateRequest. (This is a prettier version than the one I included in the third ImpostorHttpModule post.)

Now at which stage do we want to patch into the ASP.NET pipeline? As a general rule, you want to patch in as early as possible. For instance, if we were only concerned with the time of day, we would patch into the BeginRequest. The reason for patching in as early as possible is that the later you patch in, the more work ASP.NET has done. If you’re going to display a static page (or at least one with low processing overhead), try to do as little work as possible in making that determination.

Now let’s see why you might want to patch in at other places…

If you need to know who is accessing the site… PostAuthenticateRequest.

If you need to know whether that user is authorized to access a particular resource… PostAuthorizeRequest.

If you only want to execute if there is no cached content for that resource… PostResolveRequestCache.

If you need to know which handler will be executed… PostMapRequestHandler.

If you need something that you squirreled away in session state… PostAcquireRequestState.

You get the idea. Depending on what information you require, you will want to patch in at a particular stage. For our purposes, we need to know if the user is in the Administrators group. The user’s group memberships are determined in AuthorizeRequest. So we need to hook into PostAuthorizeRequest.

public void Init(HttpApplication context) {
    context.PostAuthorizeRequest += new EventHandler(context_PostAuthorizeRequest);
}

Easy enough, but let’s look at the event handler as that is where the real action is happening…

void context_PostAuthorizeRequest(object sender, EventArgs e) {
    string ext = Path.GetExtension(HttpContext.Current.Request.Path).ToLowerInvariant();
    if(ext == “.aspx”) {
        TimeSpan now = DateTime.Now.TimeOfDay;
        if(now < Availability.Instance.StartTime.TimeOfDay
                || now > Availability.Instance.EndTime.TimeOfDay) {
            // If we’re outside normal hours, check to see if the user is in any
            // of the exempt roles. By default, only members of the SiteAvailabilityExempt
            // role are exempt from site availability constraints.
            if(!HttpContext.Current.User.IsInRole(Availability.Instance.ExemptRole)) {
                HttpContext.Current.RewritePath(Availability.Instance.SiteUnavailablePage);
            }                   
        }
    }
}

We first check to see if we’re dealing with a request for an ASPX page. Remember, we’re patched into the ASP.NET pipeline, which in ASP.NET 2.0 means that we’re being called when serving up both dynamic and static content. So if we’re dealing with a request for an image, cascading stylesheet (CSS), JavaScript file, or other non-ASPX content, we better not serve up the site unavailable page! We need to allow requests for resources to pass through unaltered. Hence the check for whether we’re dealing with an ASPX page or not.

Next part is simple. Are we outside of the normal hours of operation? The Availability class is derived from System.Configuration.ApplicationSettingsBase and leverages off the new System.Configuration features. We set our configuration properties in Web.config beneath the <configuration/> root element:

<configSections>
    <sectionGroup name=”applicationSettings” type=”System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089″>
        <section name=”JamesKovacs.Web.HttpModules.Availability” type=”System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089″/>
    </sectionGroup>
</configSections>

<applicationSettings>
    <JamesKovacs.Web.HttpModules.Availability>
        <setting name=”StartTime” serializeAs=”String”>
            <value>8:00 AM</value>
        </setting>
        <setting name=”EndTime” serializeAs=”String”>
            <value>4:00 PM</value>
        </setting>
    </JamesKovacs.Web.HttpModules.Availability>
</applicationSettings>

The class defines some sensible default values so that we can run without any configuration in Web.config.

If we’re outside of regular hours, we need to find out if the user is in the exempt role, which by default is a local group called SiteAvailabilityExempt. If we’re not a member of the exempt role, we re-write the URL to point to SiteUnavailablePage, which by default is ~/SiteUnavailable.aspx.

Let’s look at what is actually happening because it’s not a redirect or transfer. We are in PostAuthorizeRequest. We don’t decide which ASPX page we will be executing until the step marked “IHttpHandler Created”. So we haven’t created the HttpHandler, or System.Web.UI.Page-derived class, in this case. HttpContext.Current.RewritePath() actually changes the URL of the request while the request is “in-flight”. So no matter which page the user browses to, she will see the contents of SiteUnavailable.aspx. She won’t know it’s SiteUnavailable.aspx, but it is. The URL will show http://www.example.com/Default.aspx or whatever page was originally requested. Think about how powerful this technique is. If we redirected to SiteUnavailable.aspx, the user could refresh her browser all day and she would always see SiteUnavailable.aspx even if the clock rolled over into business hours. To the server, you’re requesting SiteUnavailable.aspx and that’s what I’m going to serve up. With URL rewriting, we keep the original URL, but serve up alternate content. If the user refreshes and we’ve rolled into business hours, the SiteAvailabilityModule lets the request through unaltered and the user sees the correct page. Cool or what?

You can download the code here. I’ve included all the HttpModules from the Calgary Code Camp in addition to an updated version of the SiteAvailabilityModule. As always, feedback is appreciated. Enjoy!

[EDIT -- Corrected text to match the code. Thanks, Lance, for catching the error.]