ASP.NET MVC 5 CancellationToken fix

Hey everyone!

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 🙂

4 thoughts on “ASP.NET MVC 5 CancellationToken fix”

  1. I am trying your solution and have found that it works great, but I see people online saying that CancellationTokenSource must be disposed. Some have had trouble with memory leaks when it is not disposed properly.

    Have you experienced any issues with memory leaks?
    Do you have any ideas for how to dispose of these linked token sources?

Leave a Reply

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

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.