[ASP.NET Core] bind to CancellationToken object

Posted by bsgrules on Sun, 06 Mar 2022 10:54:17 +0100

The HttpContext object that manages the HTTP request context has a property called RequestAborted. According to its name, it can be used to indicate whether the client request has been cancelled.

Sure enough, its type is CancellationToken. This guy is a structure type. Why emphasize structure - because it is a value type. In the whole context transfer process of accessing HTTP, direct assignment will copy multiple instances. If it is not done well, the state data during a request communication will be inconsistent. Therefore, when passing the attribute value inside the class library, it will use the variable of object type to refer to its value. Well, yes, it is "boxing". Operating it by reference type can avoid data inconsistency caused by object replication.

See the source code of CancellationTokenModelBinder class (namespace: Microsoft.AspNetCore.Mvc.ModelBinding.Binders).

public class CancellationTokenModelBinder : IModelBinder
{
    /// <inheritdoc />
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // We need to force boxing now, so we can insert the same reference to the boxed CancellationToken
        // in both the ValidationState and ModelBindingResult.
        //
        // DO NOT simplify this code by removing the cast.
        var model = (object)bindingContext.HttpContext.RequestAborted;
        bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

All of you should be able to understand the above contents. What if you don't understand? It's okay. Don't feel inferior. You don't have to jump into the river. You can't understand it, but you know how to use it.

Focus on this sentence:

var model = (object)bindingContext.HttpContext.RequestAborted;

Cast it into object type and assign a value to ensure that the CancellationToken instance is not copied after assignment.

If the client closes (cancels) the connection after HTTP submission and before the server returns the message to the client after processing (for example, close the browser, click "Cancel" request, the network is disconnected, the router is on fire, etc.), then through httpcontext With the requestaborted attribute, we can get the relevant information in the server code. To be more direct, IsCancellationRequested will return true.

Lao Zhou will not talk about model binding for the time being. Let's see how to use the RequestAborted attribute first.

Let's give an example. Once upon a time, there was a controller named happy. It had two sons. The eldest was Index and the second was Chou Jiang. Happy's family opened a lottery shop, and the boss Index was responsible for the facade to welcome North and South tourists; The second is responsible for the business, including telling the guests the results of the lottery.

    public class HappyController : Controller
    {
        // Used to randomly generate lucky numbers
        private static readonly Random rand = new((int)DateTime.Now.ToBinary());

        // Record the log and solve the instantiation problem through dependency injection
        private readonly ILogger logger;
        /// <summary>
        ///Constructor
        /// </summary>
        ///< param name = "logfac" > get < / param > from dependency injection
        public HappyController(ILoggerFactory logfac)
        {
            logger = logfac.CreateLogger("Demo Log");
        }

        public IActionResult Index()
        {
            // Facade Kung Fu, open the door to welcome guests
            return View("~/views/TestView1.cshtml");
        }

        public async Task<IActionResult> ChouJiang()
        {
            // Lottery simulation
            int x = 5;
            int result = 0;
            // Draw five times and choose the last lucky number
            while(x > 0)
            {
                // If the connection hangs up, bye
                if(HttpContext.RequestAborted.IsCancellationRequested)
                {
                    logger.LogInformation("Request cancelled");
                    return NoContent();
                }
                await Task.Delay(500);  //Analog delay
                x--;
                result = rand.Next(0, 1000);//Generate random number
            }
            // Big prize
            return Content($"<script>alert('Lucky number:{result}')</script>", "text/html", Encoding.UTF8);
        }
    }

The core of this example is to judge httpcontext RequestAborted. Whether iscancellationrequested is true. If so, then this round of lucky draw is over.

The following Razor code is the decoration effect of Happy lottery shop. Please design it by Lao Wang next door.

@{
    ViewBag.Title = "demonstration-1";
}

<p>Click the link below to open the lucky award in the year of the tiger</p>
<a target="_blank" asp-action="ChouJiang" asp-controller="Happy">luck draw</a>

Run the example.

Click the link on the page. If you have enough patience and wait for it to complete the lottery, you will see the lucky number.

If you find it boring, after clicking the link, click "X" on the browser to cancel the operation, and you will see the log output, indicating that the connection is disconnected / the request is cancelled.

Access httpcontext easily RequestAborted. Iscancellationrequested is not very convenient, at least not as convenient as instant noodles. Therefore, we need to make a further upgrade - using model binding.

The requirements are:

  1. The bound object type is CancellationToken
  2. Binding targets can be action method parameters, Controller properties (MVC), or Model Page properties (Razor Pages).

Therefore, the above lottery code can be changed as follows:

        public async Task<IActionResult> ChouJiang(CancellationToken ct)
        {
            // ......
            while(x > 0)
            {
                // If the connection hangs up, bye
                if(ct.IsCancellationRequested)
                {
                    logger.LogInformation("Request cancelled");
                    return NoContent();
                }
                await Task.Delay(500);  //Analog delay
                x--;
                result = rand.Next(0, 1000);//Generate random number
            }
            // ......
        }

You can also define attributes in the Controller to bind. Modify this example.

        // This is an attribute
        [BindProperty(SupportsGet = true)]
        public CancellationToken CancelTK { get; set; }

        public async Task<IActionResult> ChouJiang()
        {
            // ......
            while(x > 0)
            {
                // Bye, if you hang up, bye
                if(CancelTK.IsCancellationRequested)
                {
                    //......
                }
                await Task.Delay(500);  //Analog delay
                x--;
                result = rand.Next(0, 1000);//Generate random number
            }
            // ......
        }

If you bind with a property, it is necessary to apply the bindproperty attribute to the property. Here we want to set SupportsGet to true, because in the old week example, the view is clicking the link and calling the lottery code, which is requested by HTTP-GET, and the default is that BindProperty is not bound in GET mode. Therefore, in order to bind smoothly, you have to change supportsget to true; If you use POST mode to trigger, you don't need to set it.

Topics: ASP.NET .NET