Middleware Madness: Site Maintenance In ASP.NET Core

Ideally, we would never have to take our site offline to do maintenance. All of our deployments would happen smoothly, and we could transition seamlessly into our new set of features. Once in a while it just isn’t possible, and we have to stop our users from interacting with our application. With IIS, you can drop an app_offline.htm file in the root of your site, but this requires the deployment of a file to your production environment. It works, but it isn’t great.

Maintenance mode doesn’t just affect your users, but also may impact potential users via SEO crawlers. According to this post from Yoast.com, crawlers expect to see an HTTP status code of 503.

you have to send a 503 status code in combination with a Retry-After header. Basically you’re saying: hang on, we’re doing some maintenance, please come back in X minutes. That sounds a lot better than what a 404 error says: “Not Found”. A 404 means that the server can’t find anything to return for the URL that was given.

Given that information and the necessary maintenance of a site, we decided to write middleware for support instances.

publicclassMaintenanceMiddleware{privatereadonlyRequestDelegatenext;privatereadonlyILoggerlogger;privatereadonlyMaintenanceWindowwindow;publicMaintenanceMiddleware(RequestDelegatenext,MaintenanceWindowwindow,ILogger<MaintenanceMiddleware>logger){this.next=next;this.logger=logger;this.window=window;}publicasyncTaskInvoke(HttpContextcontext){if(window.Enabled){// set the code to 503 for SEO reasonscontext.Response.StatusCode=(int)HttpStatusCode.ServiceUnavailable;context.Response.Headers.Add("Retry-After",window.RetryAfterInSeconds.ToString());context.Response.ContentType=window.ContentType;awaitcontext.Response.WriteAsync(Encoding.UTF8.GetString(window.Response),Encoding.UTF8);}awaitnext.Invoke(context);}}publicclassMaintenanceWindow{privateFunc<bool>enabledFunc;privatebyte[]response;publicMaintenanceWindow(Func<bool>enabledFunc,byte[]response){this.enabledFunc=enabledFunc;this.response=response;}publicboolEnabled=>enabledFunc();publicbyte[]Response=>response;publicintRetryAfterInSeconds{get;set;}=3600;publicstringContentType{get;set;}="text/html";}publicstaticclassMaintenanceWindowExtensions{publicstaticIServiceCollectionAddMaintenance(thisIServiceCollectionservices,MaintenanceWindowwindow){services.AddSingleton(window);returnservices;}publicstaticIServiceCollectionAddMaintenance(thisIServiceCollectionservices,Func<bool>enabler,byte[]response,stringcontentType="text/html",intretryAfterInSeconds=3600){AddMaintenance(services,newMaintenanceWindow(enabler,response){ContentType=contentType,RetryAfterInSeconds=retryAfterInSeconds});returnservices;}publicstaticIApplicationBuilderUseMaintenance(thisIApplicationBuilderbuilder){returnbuilder.UseMiddleware<MaintenanceMiddleware>();}}

Using the middleware is straight forward. First register the MaintenanceWindow class.

It is middleware, and order of registration matters. It will circumvent anything registered after it. If you still want to serve static files from the site while in maintenance mode, register it after the StaticFileMiddleware.

The maintenance window assumes HTML is the default response, but this could also work for an API that returns JSON.

Encoding is assumed to be UTF8, but the code can be modified to support any other encoding type.

You need to decide what a valid Retry-After period is. By default, we chose 3600 seconds.

The MaintenanceWindow class needs to be registered with the IoC in ASP.NET Core.

Anything in your app can trigger the ‘Enabled’ boolean, and we would assume most will pick a configuration value.

You will need to determine where the response is loaded from. In the example above, we just hardcoded some HTML.

There you have it, may all your maintenance tasks go smoothly and may your SEO be unaffected.