How to take an ASP.NET MVC web site “Down for maintenance”

Last Updated: June 18, 2016 | Created: December 22, 2015

I am working on an ASP.NET MVC5 e-commerce site and my focus is on how to apply database migrations to such a site (see previous articles on this subject). I have decided that for complex database changes I will take the web site “Down for maintenance” while I make the changes. This article details how, and why, I implemented this feature.

Having researched how to at a site offline I found some very good examples (see Khalid Abuhakmeh‘s good post and this helpful stackoverflow question & answer by FaNIX). They were really helpful (thanks guys) but my needs were slightly different. This article, which forms part of my series on Handling Entity Framework database migrations in production, describes my solution.

What I am trying to achieve

I want to create commands that allow me to stop all users, other than an admin user, accessing the web site. I am doing this to stop the database being accessed so that I can carry out a database migration. A database migration is normally followed by an updating the software, and I want the new software to obey any ‘offline’ mode that the last software has. This allows my admin user, or the automation system, to check the new site before bringing it back online.

From the customer point of view want to have friendly messages so that they know what is happening. I especially don’t want someone to be half way through placing an order and then lose it because the site suddenly goes down. That is definitely a way to upset customers!

Bring all these points together then the process for taking the web site down, doing the migration, checking it and bring it back up had the following requirements:

  1. I want to warn my customers, so I put up a “the site will go down for maintenance in xx minutes”.
  2. When the site is down it should:
    1. Show a useful message to the customers.
    2. Return a 503 Service Unavailable HTTP status code (so that the web crawler don’t index the pages).
    3. Still allow my admin user to access to the site so that I can check the upgrade had worked before maying the site live again.
  3. If I upload new software then that also should come up as “down for maintenance” until the admin user has checked it out and makes it back online.
  4. Finally my site is Microsoft’s Azure Cloud offering so I have the following technical requirements:
    1. I cannot log in locally, which affects how I know who is the admin user.
    2. I also want a solution that will handle scaling up on Azure, i.e. it must work if I have multiple instances of the web site running. This affects how I store the offline information.

Why didn’t I use app_offline.htm?

While adding  a file called app_offline.htm to your root directory is a very quick and simple way of taking an ASP.NET web site offline (see article by Scott Guthrie) doing some research pointed out some issues. Khalid Abuhakmeh‘s post points out that app_Offline.htm could cause problems with search engines (read his post for a fuller list of issues).

However the biggest issue for me was the app_offline.htm approach does not let anyone access the site. I wanted to be able to log in and test the site after an upgrade but before making it live again (requirement 2.3). That single requirement meant I had to find another way.

The other plus side to the solution I developed is a bit specific to my application, but it is worth mentioning. My solution uses Azure WebJobs which access the database. I therefore needed to stop these from accessing the database during the upgrade. There were a number of ways I could have done that, but it turns out that WebJobs can see the special offline file (described later), which can be used to put the WebJobs into a ‘stalled’ state during the upgrade. This meant my solution could stop the whole site including associated background tasks.

Using an Action Filter to redirect users

As Khalid Abuhakmeh and FaNIX suggested the best way to implement this is by adding a MVC Action Filter. This intercepts each action call and allows you to change what happens. Here is my Action Filter code:

public class OfflineActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var ipAddress = HttpContext.Current.Request.UserHostAddress;
        var offlineHelper = new OfflineHelper(ipAddress, 
              filterContext.HttpContext.Server.MapPath);

        if (offlineHelper.ThisUserShouldBeOffline)
        {
            //Yes, we are "down for maintenance" for this user
            if (filterContext.IsChildAction)
            {
                filterContext.Result = 
                     new ContentResult { Content = string.Empty };
                return;
            }
                
            filterContext.Result = new ViewResult
            {
                ViewName = "Offline"
            };
            var response = filterContext.HttpContext.Response;
            response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
            response.TrySkipIisCustomErrors = true;
            return;
        }

        //otherwise we let this through as normal
        base.OnActionExecuting(filterContext);
    }
}

The decision as to whether we should tell the user that the site is “down for maintenance” is done by the OfflineHelper class, which I will describe later, and its sets the ThisUserShouldBeOffline property to true. If true (see test on line 9) then we stop the normal page display and redirect them to the “Offline.cshtml” view while also setting the StatusCode to ServiceUnavailable (503) so web crawlers won’t index the pages while offline.

This action filter needs to be run on all actions. To do this we add it to the GlobalFilters.Filters in the Global.asax.cs file, e.g.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    //we add the filter to handle "down for maintenance"
    GlobalFilters.Filters.Add(new OfflineActionFilter());
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    ... etc.

The OfflineHelper class

I had to decide how the application would know it was in “down for maintenance” mode. In the end I decided to use the absence/presence of simple text file to control the mode. Using a file seems a bit archaic, but it fits the requirements:

  • I didn’t want to use the database, as I want the database quiescent during migrations.
  • A file would be read when new software is loaded, which would continue to be in “down for maintenance” mode until the admin user used a command to delete the file.
  • A simple file will work with Azure’s multiple instances when you scale up because all instances share the same local file system (see this helpful stackoverflow answer).
  • It allows an automated deployment script to take the site offline by writing an appropriately formatted file, and take it back online by deleting the file.
  • Plus if anything goes wrong I can use FTP to manually read, write or delete the file.

Here is the code from the OfflineHelper class:

public class OfflineHelper
{
    public static OfflineFileData OfflineData { get; private set; }

    /// <summary>
    /// This is true if we should redirect the user to the Offline View
    /// </summary>
    public bool ThisUserShouldBeOffline { get; private set; }

    public OfflineHelper(string currentIpAddress, 
          Func<string, string> mapPath)
    {

        var offlineFilePath = 
            mapPath(OfflineFileData.OfflineFilePath);
        if (File.Exists(offlineFilePath))
        {
            //The existence of the file says we want to go offline

            if (OfflineData == null)
                //We need to read the data as new file was found
                OfflineData = new OfflineFileData(offlineFilePath);

            ThisUserShouldBeOffline = DateTime.UtcNow.Subtract(
                OfflineData.TimeWhenSiteWillGoOfflineUtc).TotalSeconds > 0 
                && currentIpAddress != OfflineData.IpAddressToLetThrough;
        }
        else
        {
            //No file so not offline
            OfflineData = null;
        }
    }
}

As you can see from the code the absence of the ‘offline file.txt’ file is a simple test as to whether we should even consider being offline. If there is a file, and it hasn’t already been read in then we read it. Using this approach means we only take the performance hit of reading the file once, which is done via the OfflineFileData class (explained later).

If the offline file exists then there is a test to see if this user is allowed to access the site. If the time for the site to be offline hasn’t happened yet, or if the user is coming on the stored IP (which we will see later is taken from the authorised person who set the site to go offline) then the user is let through.

As we will see later the static OfflineData property is useful for showing messages.

The OfflineFileData class

The OfflineFileData class is in charge of the offline file and showing/changing its content. The class is a bit long so I will show it as two parts: a) the reading of the file, which is done when creating the OfflineFileData class, and b) the GoOffline and RemoveOffline commands

a) Reading the Offline File

The code is as follows:

public class OfflineFileData
{
    internal const string OfflineFilePath = "~/App_Data/offline file.txt";
    //The format of the offline file is three fields separated by the 'TextSeparator' char
    //a) datetimeUtc to go offline
    //b) the ip address to allow through
    //c) A message to show the user

    private const char TextSeparator = '|';

    private const string DefaultOfflineMessage = 
        "The site is down for maintenance. Please check back later";

    /// <summary>
    /// This contains the datatime when the site should go offline should be offline
    /// </summary>
    public DateTime TimeWhenSiteWillGoOfflineUtc { get; private set; }

    /// <summary>
    /// This contains the IP address of the authprised person to let through
    /// </summary>
    public string IpAddressToLetThrough { get; private set; }

    /// <summary>
    /// A message to display in the Offline View
    /// </summary>
    public string Message { get; private set; }

    public OfflineFileData(string offlineFilePath)
    {
        var offlineContent = File.ReadAllText(offlineFilePath)
            .Split(TextSeparator);

        DateTime parsedDateTime;
        TimeWhenSiteWillGoOfflineUtc = DateTime.TryParse(offlineContent[0], 
              null, System.Globalization.DateTimeStyles.RoundtripKind,
              out parsedDateTime) ? parsedDateTime : DateTime.UtcNow;
        IpAddressToLetThrough = offlineContent[1];
        Message = offlineContent[2];
    }
    //more below ....
}

The code is fairly obvious. It reads in three fields which it expects in the file and sets the three properties.

  1. TimeWhenSiteWillGoOfflineUtc: The DateTime in UTC format as to when the site should be offline.
  2. IpAddressToLetThrough: The IP of the admin person that put the site into offline mode, so we can let that particular person through.
  3. Message: An message that the admin person can give, like “Expect to be back by 9:30 GMT”

UPDATE: There was a bug in the parsing of the date, which did not take into account the UTC Z on the end. I have updated the DateTime.TryParse call to use the System.Globalization.DateTimeStyles.RoundtripKind style which has fixed it.

b) The GoOffline and RemoveOffline commands

The last part of the OflineFileData class contains the commands to put the site into, and take up out of, offline mode. I build these as static methods and are shown below:

public static void SetOffline(int delayByMinutes, string currentIpAddress, 
      string optionalMessage, Func<string, string> mapPath)
{
    var offlineFilePath = mapPath(OfflineFilePath);

    var fields = string.Format("{0:O}{1}{2}{1}{3}",
        DateTime.UtcNow.AddMinutes(delayByMinutes), TextSeparator, 
        currentIpAddress, optionalMessage ?? DefaultOfflineMessage);

    File.WriteAllText(offlineFilePath, fields);
}

public static void RemoveOffline(Func<string, string> mapPath)
{
    var offlineFilePath = mapPath(OfflineFilePath);
    File.Delete(offlineFilePath);
}

I think the code is fairly self explanatory. Note that the use of the the Func<string, string> mapPath is used to pass in the Server.MapPath method from the MVC Action. This also allows the code to be easily Unit Tested.

The Offline.cshtml View

The View ‘Offline.cshtml file is placed in the Views/Shared directory and looks like this (note: I am using bootstrap for my CSS)

@{
    ViewBag.Title = "Offline";
}

<div class="container">
    <div class="row">
        <div class="col-md-12 text-center">
            <h2>Our site is not currently available.</h2>
            <h3>@LabelApp.Utils.OfflineHelper.OfflineData.Message</h3>
        </div>
    </div>
</div>

As you can see this fairly simple view that says the site is offline and also shows a message that the admin person entered when GoOffline is called. This could be something like “We expect to be back up at 7am”.

The MVC Actions

The last piece of the puzzle is the MVC actions that the admin user calls to go offline or return to normal working. They are pretty simple, but I give them in case you aren’t sure how to do this. The actions go inside a Controller, which I haven’t shown and you need some sort of way to make them visible in the menu when a user with the Admin rule is logged in.

[Authorize(Roles = "Admin")]
public ActionResult GoOffline()
{
    return View(new OfflineModel());
}

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public ActionResult GoOffline(OfflineModel dto)
{
    OfflineFileData.SetOffline(
         dto.DelayTillOfflineMinutes, 
         HttpContext.Request.UserHostAddress, 
         dto.Message, 
         HttpContext.Server.MapPath);
    return View("Index");
}

[Authorize(Roles = "Admin")]
public ActionResult GoOnline()
{
    OfflineFileData.RemoveOffline(HttpContext.Server.MapPath);
    return View("Index");
}

Again, fairly straightforward. Note that the ‘HttpContext.Request.UserHostAddress’ returns the IP address of the current user as a string. This is stored in the offline test file so that we can let that user through the offline check. Also OfflineModel model contains an int property called DelayTillOfflineMinutes and the string Message that the admin person can optionally add.

What I have not shown

I have not shown the simple banner that appear when the web site is set to offline. This is added to the default layout file, normally called _Layout.cshtml in the Views/Shared folder. It accesses the static property OfflineData in the OfflineHelper class and if not null can calculate and show the time till the site goes offline as a warning to the user.

Also, in my system I give feedback to the admin user that the system after the offline/online calls as, from their point of view, nothing has obviously changed.

Down sides of this approach

I always like to look at the down sides of any approach I use. When architecting any system there is nearly always a trade off to be had. In this case we are putting an Action Filter that is called on every action call, which has a performance impact. The main performance costs are:

  1. Checking if the file exists.
  2. Reading the file content.

In my first version I read the file every time, which if there was a file then we had a 3ms with +- 3ms deviation overhead. In the newer version I only read the file on the first time we find it. This improved the performance for the case where the offline file exists.

I have instrumented the creation of the OfflineHelper in the  OfflineActionFilter and 99% of the time is in the OfflineHelper, which you would expect. When running on a B1 Basic single core , i.e. not very powerful, and the time that the OfflineHelper takes are:

  • When online: average 0.3ms, +-0.1ms deviation
  • When offline or going offline : average .4ms, +- 0.1ms deviation

Note: Has approx 6ms cost when first read of file.

Clearly there is a cost to using this approach, as File.Exists() takes some time. It would be possible to add some caching, i.e. you only look for the file if more than x seconds has passed since you did so. At the moment I am happy to live with these figures.

Other than that I cannot see any other major problems with this approach.

Conclusion

There are many ways to handle taking an ASP.NET MVC web application “down for maintenance” but for my particular requirements when doing data migrations this seems a good choice. I particularly like that if you upload new software it restarts still in offline mode, which is what I want as I can then check the migration + new code works before taking it back online.

Please do have a look at the series Handling Entity Framework database migrations in production for more on database migrations. It is a big subject, but important.

I hope this helps you. Please do feed back if you have any suggestions.

Happy coding!