Maintenance Mode Plugin – Part 2

Maintenance Mode Plugin – Part 2

Last time I wrote about a new plugin I created for putting a Blazor server-side website into maintenance mode. This time I thought I would walk through the internals of that plugin.

If you haven’t already read about my Blazor plugins then I suggest you do that first, HERE, HERE, and HERE.

Like any plugin, this one starts with a module class. That class looks like this:

public class Module : ModuleBase
{
    public override void ConfigureServices(
        IServiceCollection serviceCollection,
        IConfiguration configuration
        )
    {
        Guard.Instance().ThrowIfNull(serviceCollection, nameof(serviceCollection))
            .ThrowIfNull(configuration, nameof(configuration));

        serviceCollection.ConfigureOptions<PluginOptions>(
            DataProtector.Instance(), // <-- default data protector.
            configuration
            );

        serviceCollection.AddSingleton<MaintenanceModeRule>();

        serviceCollection.AddSingleton<
            IMaintenanceModeService, 
            MaintenanceModeService
            >();
    }

    public override void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment env
        )
    {
        Guard.Instance().ThrowIfNull(app, nameof(app))
            .ThrowIfNull(env, nameof(env));

        var rule = app.ApplicationServices.GetRequiredService<MaintenanceModeRule>();
        app.UseRewriter(new RewriteOptions().Add(rule));
    }
}

The module class is where the plugin framework registers any required services, and performs any required startup logic, for the plugin. Think of the module class as something akin to the Startup class, in a typical Blazor application.

As we can see, this Module class starts in the ConfigureServices method, where it registers a PluginOptions type. That type holds all the configuration settings for this plugin. Next, the method registers a type called MaintenanceModeRule. MaintenanceModeRule is where we’ll handle rewriting URL’s, at runtime. This way, nobody can sneak around our maintenance page and try to get any work done when we don’t want them to. Finally, the ConfigureServices method registers the IMaintenanceModeService service using the MaintenanceModeService type. IMaintenanceModeService is the service we’ll use to determine when we’re in maintenance mode and when we aren’t.

We’ll cover all the types I just mention later in this article.

The next method in the Module class is called Configure. The Configure method is used to call any startup logic required for the plugin. In our case, we fetch an instance of our MaintenanceModeRule type, using the DI container built into Blazor. So, why create an instance that way, and not by simply calling the new operator? Because, by using the DI container to stand up our MaintenanceModeRule instance, we can rely on the DI container to inject any other types we need, in the MaintenanceModeRule class’ constructor. If we had used the new operator, we’d be on the hook to also standup any required types for that constructor. Too much work for me …

That’s about it for the Module type. After the plugin framework has called the ConfigureServices and Configure methods, the plugin is ready for use.

Next let’s look at the MaintenanceModeRule type. This class is a custom URL rewriter rule that, when plugged into ASP.NET, rewrites incoming HTTP requests, on the fly. In our case, we’ll use that ability to ensure nobody sneaks around our maintenance page. Here’s what that class looks like:

internal class MaintenanceModeRule : IRule
{
    private readonly ILogger<MaintenanceModeRule> _logger;
    private readonly IMaintenanceModeService _service;

    public MaintenanceModeRule(
        IMaintenanceModeService service,
        ILogger<MaintenanceModeRule> logger
        )
    {
        Guard.Instance().ThrowIfNull(service, nameof(service))
            .ThrowIfNull(logger, nameof(logger));

        _service = service;
        _logger = logger;
    }

    public void ApplyRule(RewriteContext context)
    {
        Guard.Instance().ThrowIfNull(context, nameof(context));

        if (_service.IsInMaintenanceMode())
        {
            if (!context.HttpContext.Request.Path.StartsWithSegments("/maintenance"))
            {
                _logger.LogInformation(
                    $"Rewriting url '{context.HttpContext.Request.Path.Value}' to '/maintainance'"
                    );
                context.HttpContext.Request.Path = "/maintenance";
            }
        }
        else
        {
            if (context.HttpContext.Request.Path.StartsWithSegments("/maintenance"))
            {
                _logger.LogInformation(
                    $"Rewriting url '{context.HttpContext.Request.Path.Value}' to '/'"
                    );
                context.HttpContext.Request.Path = "/";
            }
        }
    }
}

Looking at the source, we see that the constructor accepts an IMaintenanceModeService object and an ILogger. We save both references in private fields, for when we eventually need them.

The class implements the IRule type, which has a single method named ApplyRule. That method is called by ASP.NET whenever an incoming HTTP is processed. By registering our MaintenanceModeRule type as a rewriter rule, like we did in the Module.ConfiguerServices method, we inform ASP.NET that we want our ApplyRule method called every time it processes an incoming HTTP request.

Looking at the code for the ApplyRule method, we see that we first use the IsInMaintenanceMode method, on the IMaintenanceModeService instance, to determine whether we’re in maintenance mode, or not. If we are, we check the incoming request to see if the caller is asking for anything other than the maintenance page. If they are, we rewrite the incoming request’s url to point to our maintenance page. If we aren’t in maintenance mode, then we ensure that nobody can see the maintenance page by rewriting any incoming requests for that page to the site’s root.

The end result of this rule is, if we’re in maintenance mode, we can’t see any pages besides our maintenance page. If we aren’t in maintenance mode, we can see any page except our maintenance page.

As I mentioned before, the MaintenanceModeRule uses the IMaintenanceModeService type to determine whether we’re in maintenance mode, or not. Let’s go look at how that works. The interface for IMaintenanceModeService looks like this:

internal interface IMaintenanceModeService
{
    bool IsInMaintenanceMode();
}

So, an IMaintenanceModeService service simply indicates whether a website is, or isn’t, in maintenance mode. Let’s go look at our implementation of IMaintenanceModeService, which is a class called MaintenanceModeService:

internal class MaintenanceModeService : IMaintenanceModeService
{
    private readonly ILogger<MaintenanceModeService> _logger;
    private readonly IOptions<PluginOptions> _pluginOptions;

    public MaintenanceModeService(
        IOptions<PluginOptions> pluginOptions,
        ILogger<MaintenanceModeService> logger
        )
    {
        Guard.Instance().ThrowIfNull(pluginOptions, nameof(pluginOptions))
            .ThrowIfNull(logger, nameof(logger));

        _pluginOptions = pluginOptions;
        _logger = logger;
    }
    public virtual bool IsInMaintenanceMode()
    {
        if (_pluginOptions.Value.MaintenanceMode)
        {
            return true;
        }
        return false;
    }
}

The constructor accepts an PluginOptions instance, and a Logger. It then saves those objects in fields, until we need them.

Looking at the IsInMaintenanceMode method, we see that we check the value of the MaintenanceMode property, on the PluginOptions type.

{
  "Plugins": {
    "Modules": [
      {
        "MaintenanceMode": true
      }
    ]
  }
}

So, for instance, when the MaintenanceMode field is set to true in the appsettings.json file, we will be in maintenance mode, otherwise, we aren’t in maintenance mode.

The maintenance page itself is a typical Blazor razor page. The code for that looks like this:

@page "/maintenance"

@layout MaintenanceLayout

<div class="container">
    <div class="row">
        <div class="col-md-6">
            <div class="error-template">
                <h1>
                    :) Oops!</h1>
                <h2>
                    Temporarily down for maintenance</h2>
                <h1>
                    We’ll be back soon!</h1>
                <div>
                    <p>
                        Sorry for the inconvenience but we’re performing some maintenance at the moment.
                        we’ll be back online shortly!</p>
                    <p>
                        — The Team</p>
                </div>
            </div>
        </div>
        <div class="col-md-6">
            <svg class="svg-box" width="380px" height="500px" viewbox="0 0 837 1045" version="1.1"
                xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
                    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
                        <path d="M353,9 L626.664028,170 L626.664028,487 L353,642 L79.3359724,487 L79.3359724,170 L353,9 Z" id="Polygon-1" stroke="#3bafda" stroke-width="6" sketch:type="MSShapeGroup"></path>
                        <path d="M78.5,529 L147,569.186414 L147,648.311216 L78.5,687 L10,648.311216 L10,569.186414 L78.5,529 Z" id="Polygon-2" stroke="#7266ba" stroke-width="6" sketch:type="MSShapeGroup"></path>
                        <path d="M773,186 L827,217.538705 L827,279.636651 L773,310 L719,279.636651 L719,217.538705 L773,186 Z" id="Polygon-3" stroke="#f76397" stroke-width="6" sketch:type="MSShapeGroup"></path>
                        <path d="M639,529 L773,607.846761 L773,763.091627 L639,839 L505,763.091627 L505,607.846761 L639,529 Z" id="Polygon-4" stroke="#00b19d" stroke-width="6" sketch:type="MSShapeGroup"></path>
                        <path d="M281,801 L383,861.025276 L383,979.21169 L281,1037 L179,979.21169 L179,861.025276 L281,801 Z" id="Polygon-5" stroke="#ffaa00" stroke-width="6" sketch:type="MSShapeGroup"></path>
                    </g>
                </svg>
        </div>
    </div>
</div>

<div style="padding-left: 10px; padding-top: 30px" class="text-muted">
    <a style="font-size:xx-small" href="https://bootsnipp.com/snippets/m0Vyl">HTML courtesy of Bootsnipp.com</a>
</div>

Most of this HTML is copied from HERE. I used their HTML because my attempts at a maintenance page all looked super lame. Hey, I’m a coder, not an HTML designer, give me a break.

The only other thing worth mentioning is that I’m using a blank layout for the maintenance page. Here is what that layout page looks like:

@inherits LayoutComponentBase

@Body

So that’s it, really. Not a very complicated plugin, at all.

I do have future plans for this little plugin. One thing I want to do is give it the ability to send an event that I can listen for, in my background worker tasks, so that they also take a break and stop processing during a maintenance window. Also, I think it would be cool if I could specify a scheduled window for maintenance and have the website slip into, and out of, maintenance mode automatically. That way, other resources on the same server could use that time for their maintenance, as well. Finally, it might be cool to expose a secure REST interface, from this plugin, to allow external client’s the ability to put a website into maintenance mode.

All interesting ideas. I’m sure I’ll share those features once I get around to them. For now, thanks for reading and I hope you like my plugin.

As always, the source code for this project is available, for free, HERE

The NUGET package is also available for free, HERE.

Photo by Cesar Carlevarino Aragon on Unsplash