My 2016 new year’s resolutions + ASP.NET MVC 5 CancellationToken fix

Hey everyone!

I finally decided on my 2016 new year’s resolutions and I’ve decided to become more active again when it comes to my blog.

It’s been long while since my last post and I’ve matured even more. I’ve grown towards a web developer with an affinity for .NET solutions.
You can expect more stuff about AngularJS, Web Api, MVC, javascript in general, architectures, …

And to prove my intentions, I got a neat little fix for you.

Problem: CancellationToken not being cancelled upon request abort.

We stumbled upon this small bug whilst implementing a straight foward, basic MVC 5 application.
My co-worker pointed out that the following code, although valid and correct was not working as expected in MVC 5:


[Route("tools/test")]
[HttpGet]
public async Task Test(CancellationToken token)
{
	var now = DateTime.Now;

	while (DateTime.Now < now.AddMinutes(1))
	{
		if (token.IsCancellationRequested)
		{
			return Json(false);
		}

		await BotherGoogle(token).ConfigureAwait(false);
	}

	return Json(true);
}

private async Task BotherGoogle( CancellationToken token)
{
	using (var httpClient = new HttpClient())
	{
		await httpClient.GetAsync("http://www.google.be", token).ConfigureAwait(false);
	}
}

I would call this action from JavaScript and abort the XHR request on the client after 5 seconds:


function startTest() {
	var xhr = $.get('@Url.Action("Test")', function(data) {
		toastr.info(data);
	});
	toastr.info('Test started');

	setTimeout(function() {
		xhr.abort();
		toastr.info('Test aborted');
	}, 5000);
}

As soon as the request had been aborted on the client, it would take around +- 40 seconds before the cancellation token on the server was marked as cancelled.

A solution

A solution, pointed out by Muhammad Rehan Saeed on David Paquette's blog here involved linking 2 cancellation tokens together so if one cancellation token gets cancelled, so would the linked ones:


[Route("tools/test")]
[HttpGet]
public async Task Test(CancellationToken token)
{
	var now = DateTime.Now;

	var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token, Response.ClientDisconnectedToken).Token;

	while (DateTime.Now < now.AddMinutes(1))
	{
		if (linkedToken.IsCancellationRequested)
		{
			return Json(false);
		}

		await BotherGoogle(linkedToken).ConfigureAwait(false);
	}

	return Json(true);
}

With this solution in place, as soon as I cancelled the XHR request on the client, the server would pick up upon it immediatly and return the Json(false);

However, I didn't feel like having to modify all my async actions with this fix so I started looking into how MVC exactly gets the CancellationToken in the parameter to begin with.

Enter CancellationTokenModelBinder

After going through the ASP.NET MVC 5 sources i found that the model binder which makes the CancellationToken parameter work is nothing more than this:


// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

using System.Threading;

namespace System.Web.Mvc
{
    public class CancellationTokenModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            return default(CancellationToken);
        }
    }
}

I had hoped on something more impressive but that it would be this simplistic I wouldn't have thought -.-'
Okay, only made my idea easier to implement.

Step 1: Throw away the default CancellationTokenModelBinder

In Global.asax


protected void Application_Start(object sender, EventArgs e)
{
	// ... application startup code

	ModelBinders.Binders.Remove(typeof(CancellationToken));
}

Step 2: Create a new CancellationTokenModelBinder


public class FixedCancellationTokenModelBinder : IModelBinder
{
	public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	{
		var source = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken),
				controllerContext.HttpContext.Response.ClientDisconnectedToken);

		return source.Token;
	}
}

Step 3: Register the new CancellationTokenModelBinder with your application

In Global.asax


protected void Application_Start(object sender, EventArgs e)
{
	// ... application startup code

	ModelBinders.Binders.Remove(typeof(CancellationToken));
	ModelBinders.Binders.Add(typeof(CancellationToken), new FixedCancellationTokenModelBinder());
}

We can now remove the patch code from the action and it will keep working 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *