Adding a delay to ASP.NET Core Web API methods to simulate slow or erratic networks
I recently commented on a tweet by @jongalloway, saying:
A thousand times this! Run your app with an artificial delay introduced into server responses and look for things that don't make sense. There's a good chance you'll find some things that just plain break because of timing assumptions. Fixing both will be wins for your users https://t.co/cwVqq3w3Rz
— Robert Wray (@robertwrayuk) March 16, 2019
It got me thinking about ways to do this in ASP.NET Core, other than the simple and manual process of whacking a breakpoint at the start of each action and leaving it spinning for a while or adding a delay using a network monitoring tool. The answer I came up with was filters, specifically action filters. According to the documentation:
Filters in ASP.NET Core MVC allow you to run code before or after specific stages in the request processing pipeline.
This gave me exactly what I want, a way to run code (specifically some code to introduce a delay) at a specific stage of the request. For the purposes of the exercise the when didn't particularly matter to me, though if you've got server side code that relies on being hit within a specific time-frame, e.g. within a second of the client-side request being initiated, you may need to take this into account.
The filter I put together actually ended up being pretty simple:
Filter code:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
namespace CoreDelayingTactics.Filters
{
public class DelayFilter : IAsyncActionFilter
{
private int _delayInMs;
public DelayFilter(IConfiguration configuration)
{
_delayInMs = configuration.GetValue<int>("ApiDelayDuration", 0);
}
async Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await Task.Delay(_delayInMs);
await next();
}
}
}
There are two things to point out about the filter; firstly, it's not production code and should really have logging (you want something in your logs so you can see when it's active in production, right?), possibly a skip past the call to Task.Delay if the value is 0, validation of the delay value, perhaps even a #if DEBUG so that the call to Task.Delay gets compiled out in release builds. Secondly, it's using Task.Delay instead of the ubiquitous Thread.Sleep. Pretty much everything I've read says that using Thread.Sleep in async code is a bad idea (to be fair it's usually a sign that you're hacking your way round a problem even in non-async code) but Task.Delay is far more palatable. Even in code like this that isn't "production" code it's important to do things the right way so as to ensure that something isn't later taken as a "well, it's done here so it must be fine" example.
Testing it
I've used the standard ASP.NET Core template and added the filter by wiring it in through the ConfigureServices method in Startup.cs:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(options => { options.Filters.Add(typeof(DelayFilter)); }) .AddNewtonsoftJson(); }
With that done, the last thing to do is add a setting to appsettings.json so the filter actually introduces a delay:
{ "Logging": { "LogLevel": { "Default": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ApiDelayDuration": "5000" }
Now to test it, by hitting one of the sample actions that's included in the template (https://localhost:44363/api/values), which shows this result in Fiddler:
ACTUAL PERFORMANCE -------------- ClientConnected: 06:29:08.372 ClientBeginRequest: 06:29:08.449 GotRequestHeaders: 06:29:08.449 ClientDoneRequest: 06:29:08.449 Determine Gateway: 0ms DNS Lookup: 0ms TCP/IP Connect: 0ms HTTPS Handshake: 0ms ServerConnected: 06:29:08.409 FiddlerBeginRequest: 06:29:08.449 ServerGotRequest: 06:29:08.450 ServerBeginResponse: 06:29:13.728 GotResponseHeaders: 06:29:13.728 ServerDoneResponse: 06:29:13.793 ClientBeginResponse: 06:29:13.793 ClientDoneResponse: 06:29:13.794 Overall Elapsed: 0:00:05.344
That's all there is to it. Hopefully this'll come in handy for you.