1

I have on ASP.Net C# web API with an endpoint for the import. Javascript client sends a list of items to this API and API process this list in another thread (long task) and immediately returns unique id (GUID) of process. Now I need the cancel the background task from the CLIENT. Is possible to somehow send the cancelation token from the client? I have tried to add CancellationToken as a parameter to my controller async action but I don't know how to pass it from the client. For simplification, we can use as the client the Postman app.

Sample server-side

    [HttpPost]
    [UserContextActionFilter]
    [RequestBodyType(typeof(List<List<Item>>))]
    [Route("api/bulk/ImportAsync")]
    public async Task<IHttpActionResult> ImportAsync()
    {
        var body = await RequestHelper.GetRequestBody(this);
        var queue = JsonConvert.DeserializeObject<List<List<Item>>>(body);
        var resultWrapper = new AsynckResultWrapper(queue.Count);


        HostingEnvironment.QueueBackgroundWorkItem(async ct =>
        {
            foreach (var item in queue)
            {
                var result = await ProcessItemList(item, false);
                resultWrapper.AddResultItem(result);
            }
        });

        return Ok(new
        {
            ProcessId = resultWrapper.ProcessId.ToString()
        });
    }


    private async Task<ItemResult> ProcessItemList(<List<Item>>itemList, bool runInOneTransaction = false)
    {
        try
        {
            var result = await PerformBulkOperation(true, itemList);
            return new ResultWrapper(result);
        }
        catch (Exception ex)
        {

            // process exception
            return new ResultWrapper(ex);

        }
    }
Peter Bons
  • 26,826
  • 4
  • 50
  • 74
  • please read: [How do I cancel an HTTP fetch() request?](https://stackoverflow.com/questions/31061838/how-do-i-cancel-an-http-fetch-request) and [Using CancellationTokens in ASP.NET Core MVC controllers](https://andrewlock.net/using-cancellationtokens-in-asp-net-core-mvc-controllers/) – Patrick Beynio Nov 13 '20 at 09:47
  • So to be clear, you want to cancel the work that is queued on the background worker? – Peter Bons Nov 13 '20 at 10:02
  • @PeterBons yes, i want to cancel the task on the background worker. Is there any way? – Jaroslav Maly Nov 13 '20 at 10:05

1 Answers1

4

On a high level what you could do is store the process id along with a cancellation token source when you queue the work. Then you can expose a new endpoint that accepts a process id, gets the cancellation token source from the store and cancels the associated token:

        [HttpPost]
        [UserContextActionFilter]
        [RequestBodyType(typeof(List<List<Item>>))]
        [Route("api/bulk/ImportAsync")]
        public async Task<IHttpActionResult> ImportAsync()
        {
            var body = await RequestHelper.GetRequestBody(this);
            var queue = JsonConvert.DeserializeObject<List<List<Item>>>(body);
            var resultWrapper = new AsynckResultWrapper(queue.Count);

            HostingEnvironment.QueueBackgroundWorkItem(async ct =>
            {
                var lts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                var ct = lts.Token;
                TokenStore.Store(resultWrapper.ProcessId, lts);

                foreach (var item in queue)
                {
                    var result = await ProcessItemList(item, ct, false);
                    resultWrapper.AddResultItem(result);
                }

                TokenStore.Remove(processId) // remove the cancellation token source from storage when doen, because there is nothing to cancel
            });

            return Ok(new
            {
                ProcessId = resultWrapper.ProcessId.ToString()
            });
        }


        private async Task<ItemResult> ProcessItemList(<List<Item>>itemList, CancellationToken token, bool runInOneTransaction = false)
        {
            try
            {
                var result = await PerformBulkOperation(true, itemList, token);
                return new ResultWrapper(result);
            }
            catch (Exception ex)
            {

                // process exception
                return new ResultWrapper(ex);

            }
        }

        [Route("api/bulk/CancelImportAsync")]
        public async Task<IHttpActionResult> CancelImportAsync(Guid processId)
        {
            var tokenSource = TokenStore.Get(processId);
            tokenSource.Cancel();

            TokenStore.Remove(processId) // remove the cancellation token source from storage when cancelled
        }

In the above example I modified the ProcessItemList to accept a cancellation token and pass it to PerformBulkOperation, assuming that method has support for cancellation tokens. If not, you can manually call ThrowIfCancellationRequested(); on the cancellation token at certain points in the code to stop when cancellation is requested.

I've added a new endpoint that allows you to cancel a pending operation.

Disclaimer
There are for sure some things you need to think about, especially when it is a public api. You can extend the store to accepts some kind of security token and when cancellation is requested you check whether it matches with the security token that queued the work. My answer is focused on the basics of the question

Also, I left the implementation of the store to your own imagination ;-)

Peter Bons
  • 26,826
  • 4
  • 50
  • 74
  • 1
    I cannot tell whether it is safe solution - i.e. can the process be stopped by an arbitrary client? I.e. client A invokes import action, thhen some random client (other rom A) invokes CancelImportAsync? This way, client A will never get his results, even though he didn't cancell import. – Michał Turczyn Nov 16 '20 at 10:28
  • 2
    @MichałTurczyn Yeah , there are for sure some things you need to think about, especially when it is a public api. You can extend the store to accepts some kind of security token and when cancellation is requested you check whether it matches with the security token that queued the work. My answer is focused on the basics of the question. – Peter Bons Nov 16 '20 at 10:30
  • 2
    @MichałTurczyn It is not the responsibility of a Stack Overflow answer to provide a full solution that takes into account every possible permutation. It is the responsibility of the person using the answer to **use their brain** and alter the answer to fit their specific scenario. If they don't do that, it is their fault when problems crop up. – Ian Kemp Nov 16 '20 at 13:02
  • 1
    @PeterBons Thanks a lot. This is what I need. – Jaroslav Maly Nov 18 '20 at 09:32
  • 1
    How to store cancellation token source and retrieve it from another api call? – RDeveloper Apr 21 '22 at 04:45
  • 1
    @RDeveloper You could use a `Dictionary` for example. – Peter Bons Apr 21 '22 at 14:31
  • How this will work with 2 APIs? First API run QueueBackgroundWorkItem long running task. And then if client want to cancel long running task how this can be done with second api call? In second api call how do we access the dictionary? – RDeveloper Apr 22 '22 at 03:35
  • The first api could return a id, that id is used as the key in the dictionary along with the CancellationTokenSource, then the second api takes that id and uses it to cancel to corresponding work. – Peter Bons Apr 22 '22 at 06:30
  • I get that using a id as key for dictionary but where dictionary will be stored. In the second api call Dictionary will be still in memory? – RDeveloper Apr 22 '22 at 15:05
  • I would wrap it in a class and register it as a singleton so it stays in memory and can be accessed by different endpoints – Peter Bons Apr 22 '22 at 15:43