{"id":749,"date":"2016-01-07T10:42:53","date_gmt":"2016-01-07T10:42:53","guid":{"rendered":"http:\/\/dirk.schuermans.me\/?p=749"},"modified":"2017-12-11T14:01:59","modified_gmt":"2017-12-11T14:01:59","slug":"my-2016-new-years-resolutions-asp-net-mvc-5-cancellationtoken-fix","status":"publish","type":"post","link":"https:\/\/dirk.schuermans.me\/?p=749","title":{"rendered":"ASP.NET MVC 5 CancellationToken fix"},"content":{"rendered":"<p>Hey everyone!<\/p>\n<p>I got a neat little fix for you.<\/p>\n<h3>Problem: CancellationToken not being cancelled upon request abort.<\/h3>\n<p>We stumbled upon this small bug whilst implementing a straight foward, basic MVC 5 application.<br \/>\nMy co-worker pointed out that the following code, although valid and correct was not working as expected in MVC 5:<\/p>\n<pre><code class=\"c#\">\r\n[Route(\"tools\/test\")]\r\n[HttpGet]\r\npublic async Task<ActionResult> Test(CancellationToken token)\r\n{\r\n\tvar now = DateTime.Now;\r\n\r\n\twhile (DateTime.Now < now.AddMinutes(1))\r\n\t{\r\n\t\tif (token.IsCancellationRequested)\r\n\t\t{\r\n\t\t\treturn Json(false);\r\n\t\t}\r\n\r\n\t\tawait BotherGoogle(token).ConfigureAwait(false);\r\n\t}\r\n\r\n\treturn Json(true);\r\n}\r\n\r\nprivate async Task BotherGoogle( CancellationToken token)\r\n{\r\n\tusing (var httpClient = new HttpClient())\r\n\t{\r\n\t\tawait httpClient.GetAsync(\"http:\/\/www.google.be\", token).ConfigureAwait(false);\r\n\t}\r\n}\r\n<\/code><\/pre>\n<p>I would call this action from JavaScript and abort the XHR request on the client after 5 seconds:<\/p>\n<pre><code class=\"js\">\r\nfunction startTest() {\r\n\tvar xhr = $.get('@Url.Action(\"Test\")', function(data) {\r\n\t\ttoastr.info(data);\r\n\t});\r\n\ttoastr.info('Test started');\r\n\r\n\tsetTimeout(function() {\r\n\t\txhr.abort();\r\n\t\ttoastr.info('Test aborted');\r\n\t}, 5000);\r\n}\r\n<\/code><\/pre>\n<p>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.<\/p>\n<h3>A solution<\/h3>\n<p>A solution, pointed out by <a href=\"https:\/\/disqus.com\/by\/RehanSaeedUK\/\" target=\"_blank\">Muhammad Rehan Saeed<\/a> on <a href=\"https:\/\/disqus.com\/by\/davepaquette\/\" target=\"_blank\">David Paquette's<\/a> blog <a href=\"http:\/\/www.davepaquette.com\/archive\/2015\/07\/19\/cancelling-long-running-queries-in-asp-net-mvc-and-web-api.aspx\" target=\"_blank\">here<\/a> involved linking 2 cancellation tokens together so if one cancellation token gets cancelled, so would the linked ones:<\/p>\n<p><!--more--><\/p>\n<pre><code class=\"csharp\">\r\n[Route(\"tools\/test\")]\r\n[HttpGet]\r\npublic async Task<ActionResult> Test(CancellationToken token)\r\n{\r\n\tvar now = DateTime.Now;\r\n\r\n\tvar linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token, Response.ClientDisconnectedToken).Token;\r\n\r\n\twhile (DateTime.Now < now.AddMinutes(1))\r\n\t{\r\n\t\tif (linkedToken.IsCancellationRequested)\r\n\t\t{\r\n\t\t\treturn Json(false);\r\n\t\t}\r\n\r\n\t\tawait BotherGoogle(linkedToken).ConfigureAwait(false);\r\n\t}\r\n\r\n\treturn Json(true);\r\n}\r\n<\/code><\/pre>\n<p>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);<\/p>\n<p>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.<\/p>\n<h3>Enter CancellationTokenModelBinder<\/h3>\n<p>After going through the <a href=\"https:\/\/aspnetwebstack.codeplex.com\/SourceControl\/latest#src\/System.Web.Mvc\/CancellationTokenModelBinder.cs\" target=\"_blank\">ASP.NET MVC 5 sources<\/a> i found that the model binder which makes the CancellationToken parameter work is nothing more than this:<\/p>\n<pre><code class=\"csharp\">\r\n\/\/ Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.\r\n\r\nusing System.Threading;\r\n\r\nnamespace System.Web.Mvc\r\n{\r\n    public class CancellationTokenModelBinder : IModelBinder\r\n    {\r\n        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)\r\n        {\r\n            return default(CancellationToken);\r\n        }\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>I had hoped on something more impressive but that it would be this simplistic I wouldn't have thought -.-'<br \/>\nOkay, only made my idea easier to implement.<\/p>\n<h4>Step 1: Throw away the default CancellationTokenModelBinder<\/h4>\n<p><strong>In Global.asax<\/strong><\/p>\n<pre><code class=\"csharp\">\r\nprotected void Application_Start(object sender, EventArgs e)\r\n{\r\n\t\/\/ ... application startup code\r\n\r\n\tModelBinders.Binders.Remove(typeof(CancellationToken));\r\n}\r\n<\/code><\/pre>\n<h4>Step 2: Create a new CancellationTokenModelBinder<\/h4>\n<pre><code class=\"csharp\">\r\npublic class FixedCancellationTokenModelBinder : IModelBinder\r\n{\r\n\tpublic object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)\r\n\t{\r\n\t\tvar source = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken),\r\n\t\t\t\tcontrollerContext.HttpContext.Response.ClientDisconnectedToken);\r\n\r\n\t\treturn source.Token;\r\n\t}\r\n}\r\n<\/code><\/pre>\n<h4>Step 3: Register the new CancellationTokenModelBinder with your application<\/h4>\n<p><strong>In Global.asax<\/strong><\/p>\n<pre><code class=\"csharp\">\r\nprotected void Application_Start(object sender, EventArgs e)\r\n{\r\n\t\/\/ ... application startup code\r\n\r\n\tModelBinders.Binders.Remove(typeof(CancellationToken));\r\n\tModelBinders.Binders.Add(typeof(CancellationToken), new FixedCancellationTokenModelBinder());\r\n}\r\n<\/code><\/pre>\n<p>We can now remove the patch code from the action and it will keep working \ud83d\ude42<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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(&#8220;tools\/test&#8221;)] [HttpGet] &hellip; <a href=\"https:\/\/dirk.schuermans.me\/?p=749\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">ASP.NET MVC 5 CancellationToken fix<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[6],"tags":[42,39,41,40],"_links":{"self":[{"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=\/wp\/v2\/posts\/749"}],"collection":[{"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=749"}],"version-history":[{"count":15,"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=\/wp\/v2\/posts\/749\/revisions"}],"predecessor-version":[{"id":826,"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=\/wp\/v2\/posts\/749\/revisions\/826"}],"wp:attachment":[{"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=749"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=749"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dirk.schuermans.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=749"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}