ASP.NET MVC5 - Asynchronous Controllers And Cancellation Token

Asynchronous Controller helps in those situations, where a Web page has to perform a long running task and the user cannot interact with other features of the Website. Cancellation Token on the other hand is an important part of the Asynchronous Controller. Cancellation Token comes in handy, when a user wants to cancel the long running process or to navigate to a new page. In a non-asynchronous controller environment, whenever a long or slow processing is done on a Web page, the interaction of the user with the Web page will be compromised, which results in the Web page being stuck, which apparently seems like the Web Server is down. Consider a situation, where a Web Server takes about 2 to 5 minutes to complete a task and even if a user clicks page refresh, the response from the Web Server seems non-responsive. This happens because when a user clicks refresh, there is no way to signal the running process, when the user has canceled the process. Hence, the process will continue to perform and the moment the process completes the action, the page will proceed with the designated user action. In such a scenario, it is important that at each cancellation, running process should be signaled and the corresponding proceeding task like long insertion of database should also be informed about the cancelling signal. The need of cancellation token is beautifully explained by Dave Paquette in his post on Cancelling Long Running Queries in ASP.NET MVC and Web API.

In today's tutorial, I will demonstrate how to write an asynchronous controller with a cancellation token along with configuration of the Web Application to stay alive until the long running process completes.


Prerequisites

The prerequisites include knowledge about the following technologies.
  1. ASP.NET MVC5.
  2. ADO.NET.
  3. Entity Framework.
  4. HTML.
  5. Javascript.
  6. AJAX.
  7. CSS.
  8. Bootstrap.
  9. C# programming.
  10. C# LINQ.
  11. jQuery.

You can download the complete source code for this or you can follow the step by step discussion below. The sample code is developed in Microsoft Visual Studio 2015 Enterprise. I am using random tables extract from Adventure Works Sample Database. I have also placed ".BAK" file for SQL Server, in case anyone is interested to execute the solution.

  1. Create new MVC5 Web Application project and name it "ImmediateRefreshWithLongDBQuery".
  2. Open "_Layout.cshtml" file under "Views->Shared" folder and replace the existing code with the following:
    1. <!DOCTYPE html>    
    2.  <html>    
    3.  <head>    
    4.    <meta charset="utf-8" />    
    5.    <meta name="viewport" content="width=device-width, initial-scale=1.0">    
    6.    <title>@ViewBag.Title</title>    
    7.    @Styles.Render("~/Content/css")    
    8.    @Scripts.Render("~/bundles/modernizr")    
    9.    <!-- Font Awesome -->    
    10.    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" />    
    11.    <!-- Data table -->    
    12.    <link rel="stylesheet" href="https://cdn.datatables.net/1.10.10/css/dataTables.bootstrap.min.css " />    
    13.    @* Custom *@    
    14.    @Styles.Render("~/Content/css/custom-style")    
    15.  </head>    
    16.  <body>    
    17.    <div class="navbar navbar-inverse navbar-fixed-top">    
    18.      <div class="container">    
    19.        <div class="navbar-header">    
    20.          <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">    
    21.            <span class="icon-bar"></span>    
    22.            <span class="icon-bar"></span>    
    23.            <span class="icon-bar"></span>    
    24.          </button>    
    25.        </div>    
    26.        <div class="navbar-collapse collapse">    
    27.          <ul class="nav navbar-nav">    
    28.            <li>@Html.ActionLink("Home", "Index", "Home")</li>    
    29.            <li>@Html.ActionLink("Slow Page", "IndexSlow", "Home")</li>    
    30.          </ul>    
    31.        </div>    
    32.      </div>    
    33.    </div>    
    34.    <div class="container body-content">    
    35.      @RenderBody()    
    36.      <hr />    
    37.      <footer>    
    38.        <center>    
    39.          <p><strong>Copyright © @DateTime.Now.Year - <a href="http://asmak9.blogspot.com/">Asma's Blog</a>.</strong> All rights reserved.</p>    
    40.        </center>    
    41.      </footer>    
    42.    </div>    
    43.    @Scripts.Render("~/bundles/jquery")    
    44.    @Scripts.Render("~/bundles/bootstrap")    
    45.    <!-- Data Table -->    
    46.    <script src="https://cdn.datatables.net/1.10.10/js/jquery.dataTables.min.js" type="text/javascript"></script>    
    47.    <script src="https://cdn.datatables.net/1.10.10/js/dataTables.bootstrap.min.js" type="text/javascript"></script>    
    48.    @Scripts.Render("~/bundles/custom-datatable")    
    49.    @RenderSection("scripts", required: false)    
    50.  </body>    
    51.  </html> 
    Here, I have simply altered the existing layout and incorporated links to require the scripts and styles.

  3. Create a new page called "Index.cshtml" under "Views->Home" folder and place the code, mentioned below in it.
    1. @{    
    2.    ViewBag.Title = "Immediate Refresh with Slow DB Query";    
    3.  }    
    4.  <div class="jumbotron">    
    5.    <h1>Simple Page</h1>    
    6.    <p class="lead">This is a simple normal speed page.</p>    
    7.  </div> 
    Here, I am simply creating a page heading.
  4. Create new page called "IndexSlow.cshtml" under "Views->Home" folder and place the code, mentioned below in it.

    1. @{    
    2.    ViewBag.Title = "Immediate Refresh with Slow DB Query";    
    3.  }    
    4.  <div class="row">    
    5.    <div class="panel-heading">    
    6.      <div class="col-md-8 custom-heading3">    
    7.        <h3>    
    8.          <i class="fa fa-table"></i>    
    9.          <span>Slow Loading Page</span>    
    10.        </h3>    
    11.      </div>    
    12.    </div>    
    13.  </div>    
    14.  <div class="row">    
    15.    <section class="col-md-12 col-md-push-0">    
    16.      @Html.Partial("_ViewListPartial")    
    17.    </section>    
    18.  </div> 

    Here, I am simply creating a page heading and section for my partial view, where I will be displaying my random slow loaded list from the Web Server.

  5. Now, create a new partial page under "Views->Home" folder, name it "_ViewListPartial.cshtml" and place the code, mentioned below in it.
    1. <section>    
    2.    <div class="well bs-component">    
    3.      <br />    
    4.      <div class="row">    
    5.        <div>    
    6.          <table class="table table-striped table-bordered table-hover"    
    7.              id="TableId"    
    8.              cellspacing="0"    
    9.              align="center"    
    10.              width="100%">    
    11.            <thead>    
    12.              <tr>    
    13.                <th>Sr</th>    
    14.                <th>Title</th>    
    15.                <th>First Name</th>    
    16.                <th>Middle Name</th>    
    17.                <th>Last Name</th>    
    18.              </tr>    
    19.            </thead>    
    20.          </table>    
    21.        </div>    
    22.      </div>    
    23.    </div>    
    24.  </section> 
    Here, I have created a table holder that will be integrated with Datatables plugin with the data from the Server side. I have only provided table header information here, since the data will be integrated from the Server side.

  6. Now, create a new script file under "Scripts" folder, name it "custom-datatable.js" and place the code, mentioned below in it.
    1. $(document).ready(function ()    
    2.  {    
    3.    $('#TableId').DataTable(    
    4.    {    
    5.      "columnDefs": [    
    6.        { "width": "5%", "targets": [0] },    
    7.        { "className": "text-center custom-middle-align", "targets": [0, 1, 2, 3, 4] },    
    8.      ],    
    9.      "language":    
    10.        {    
    11.          "processing": "<div class='overlay custom-loader-background'><i class='fa fa-cog fa-spin custom-loader-color'></i></div>"    
    12.        },    
    13.      "processing": true,    
    14.      "serverSide": true,    
    15.      "ajax":    
    16.        {    
    17.          "url": "/Home/GetData",    
    18.          "type": "POST",    
    19.          "dataType": "JSON"    
    20.        },    
    21.      "columns": [    
    22.            { "data": "Sr" },    
    23.            { "data": "Title" },    
    24.            { "data": "FirstName" },    
    25.            { "data": "MiddleName" },    
    26.            { "data": "LastName" }    
    27.      ]    
    28.    });    
    29.  }); 
    In the code, mentioned above, I have configured the plugin to load the data from the Server side.

  7. I have used entity framework ADO.NET database first approach to create my model out of my stored procedure. After you have created the model from the database, you need to add following line into your "*Model.Context.tt" file. Find your model constructor in the file and add following line i.e.
    1. ((IObjectContextAdapter)this).ObjectContext.CommandTimeout = 60000; // 60 mins.   
    The line, mentioned above will allow the Web Server to process long running database queries without timing out the Web Server. I have used 60 minutes wait before timeout. We are adding the code in *.tt file because whenever the model is refreshed, our added line will be automatically added into constructor

  8. Now, in "HomeController.cs" file, add the function, mentioned below to load the data in the database.
    1. #region Load Data    
    2.      /// <summary>    
    3.      /// Load data method.    
    4.      /// </summary>    
    5.      /// <param name="cancellationToken">Cancellation token parameter</param>    
    6.      /// <returns>Returns - Data</returns>    
    7.      private async Task<List<sp_slow_test_Result>> LoadData(CancellationTokenSource cancellationToken)    
    8.      {    
    9.        // Initialization.    
    10.        List<sp_slow_test_Result> lst = new List<sp_slow_test_Result>();    
    11.        try    
    12.        {    
    13.          // Initialization.    
    14.          testEntities databaseManager = new testEntities();    
    15.          // Loading.    
    16.          lst = await databaseManager.Database.SqlQuery<sp_slow_test_Result>("EXEC sp_slow_test").ToListAsync(cancellationToken.Token);    
    17.        }    
    18.        catch (Exception ex)    
    19.        {    
    20.          // info.    
    21.          Console.Write(ex);    
    22.        }    
    23.        // info.    
    24.        return lst;    
    25.      }    
    26.      #endregion 
    In the code, mentioned above, we are loading the data from the database, using stored procedure with the signal of cancellation token pass to our resulting list conversion method.

  9. Now, let’s add our Asynchronous Controller into "HomeController.cs" file i.e.
    1. #region Get data method.    
    2.      /// <summary>    
    3.      /// GET: /Home/GetData    
    4.      /// </summary>    
    5.      /// <param name="cancellationToken">Cancellation token parameter</param>    
    6.      /// <returns>Return data</returns>    
    7.      [NoAsyncTimeout]    
    8.      public async Task<ActionResult> GetData(CancellationToken cancellationToken)    
    9.      {    
    10.        // Initialization.    
    11.        JsonResult result = new JsonResult();    
    12.        try    
    13.        {    
    14.          // Initialization.    
    15.          CancellationToken disconnectedToken = Response.ClientDisconnectedToken;    
    16.          var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, disconnectedToken);    
    17.          // Initialization.    
    18.          string search = Request.Form.GetValues("search[value]")[0];    
    19.          string draw = Request.Form.GetValues("draw")[0];    
    20.          string order = Request.Form.GetValues("order[0][column]")[0];    
    21.          string orderDir = Request.Form.GetValues("order[0][dir]")[0];    
    22.          int startRec = Convert.ToInt32(Request.Form.GetValues("start")[0]);    
    23.          int pageSize = Convert.ToInt32(Request.Form.GetValues("length")[0]);    
    24.          // Loading.    
    25.          List<sp_slow_test_Result> data = await this.LoadData(source);    
    26.          // Total record count.    
    27.          int totalRecords = data.Count;    
    28.          // Verification.    
    29.          if (!string.IsNullOrEmpty(search) &&    
    30.            !string.IsNullOrWhiteSpace(search))    
    31.          {    
    32.            // Apply search    
    33.            data = data.Where(p => p.Sr.ToString().ToLower().Contains(search.ToLower()) ||    
    34.                        p.Title.ToLower().Contains(search.ToLower()) ||    
    35.                        p.FirstName.ToString().ToLower().Contains(search.ToLower()) ||    
    36.                        p.MiddleName.ToLower().Contains(search.ToLower()) ||    
    37.                        p.LastName.ToLower().Contains(search.ToLower())).ToList();    
    38.          }    
    39.          // Sorting.    
    40.          data = this.SortByColumnWithOrder(order, orderDir, data);    
    41.          // Filter record count.    
    42.          int recFilter = data.Count;    
    43.          // Apply pagination.    
    44.          data = data.Skip(startRec).Take(pageSize).ToList();    
    45.          // Loading drop down lists.    
    46.          result = this.Json(new { draw = Convert.ToInt32(draw), recordsTotal = totalRecords, recordsFiltered = recFilter, data = data }, JsonRequestBehavior.AllowGet);    
    47.        }    
    48.        catch (Exception ex)    
    49.        {    
    50.          // Info    
    51.          Console.Write(ex);    
    52.        }    
    53.        // Return info.    
    54.        return result;    
    55.      }    
    56.      #endregion 
    In the code, mentioned above, we have created Asynchronous Controller and cancellation token has been passed to it as well. Notice here that, when we call this method, using AJAX call, we do not need to pass any parameter from AJAX method. The Browser will automatically handle the passing of the cancellation token. We only need to capture that cancellation token in our controller method.

    In the code, mentioned above, we are also using "[NoAsyncTimeout]" attribute at the method level because we do not want our Web Server to timeout our Asynchronous Controller.

  10. Open "Web.config" file and do add following line i.e.
    1. <system.web>    
    2.     <httpRuntime targetFramework="4.5" executionTimeout="100000" maxRequestLength="214748364" />    
    3.   </system.web>   
    We are adding the line, mentioned above because we do not want our Web Server to timeout before our long running process completes.
    .
  11. Now, execute the project and you will be able to see the Web page, shown below.

     
Conclusion
 
In this tutorial, you learned to write an asynchronous controller. You also learned to pass cancellation token to the asynchronous controller. You also learned to pass cancellation token to the database's long running query. You also learned to set automatic Web Server timeout in your entity framework ADO.NET database first model. You also learned the usage of no asynchronous timeout attribute used with the asynchronous controller and you will learn to configure Web Server timeout settings from the web.config file.