Sunday 8 August 2021

Api call with long running asynchronous backend with .Net core - Part1

 Hello friend. Let's assume that you expose some API (in our case REST API) to the client. The API doesn't actually do anything useful. It is just an entry point to a more complex backend operation. So since the API gets the operation if call the backend and return the result to the caller. The single flow looks like this.

simple api call
Simple from the client to the backend with 



On the backend, you have some operations. This operation can be very fast and in this case, the client will not notice at all the round trip that API did to the backend. But it also can be that the backend will run some long operation. In this case, it doesn't make sense to keep the client waiting for the long-running operation to complete. Also, we need to remember that client invoked the HTTP call to API and HTTP call can have a timeout. It is also possible that the API is protected by external services like Cloudflare that also has a default timeout. 

A more serious problem is the fact the back end is actually asynchronous and once it is invoked from the API, the API no longer has a stateful connection that he can monitor and know if the process ended or not. 

Bellow, I will propose two possible solutions to the problem. 

One is the option to accept the connection from the client, run asynchronous operation, wait for it to complete, get the result and return it on the same connection that the client opened. 

The second option is to get the request from the client, invoke the asynchronous operation, and return immediately to the client with a message, how many times the client should wait till he needs to check if there is a response ready. In this case, it will be the responsibility of the client to check for a response again.

We will first of all focus on the first option. For this, I will create 2 web API services. One will simulate API service and the second one will simulate the backend. Both services will be .Net core web API projects.  I will put pieces of the code with a inline comments and also a link to download the project

Front end API code




    /// <summary>
    /// this is out sample model
    /// </summary>
    public class ApiModel
    {
        public ManualResetEvent ResetEvent { set; get; }
        public string ApiResult { set; get; }
    }

Front end controller


[ApiController]
    [Route("[controller]")]
    public class FrontendApi : ControllerBase
    {
        private readonly IHttpClientFactory _clientFactory;
        //this dictionary will hold the information about long running jobs
        private static ConcurrentDictionary<Guid, Model.ApiModel> dict = new ConcurrentDictionary<Guid, ApiModel>();
        private readonly ILogger<FrontendApi> _logger;
        /// <summary>
        /// Constructor with dependancy injection
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="clientFactory"></param>
        public FrontendApi(ILogger<FrontendApi> logger,
                           IHttpClientFactory clientFactory)
        {
            _logger = logger;
            _clientFactory = clientFactory;
        }

        /// <summary>
        /// End point that gets the result from the backend and release the Wait
        /// </summary>
        /// <param name="guid"></param>
        /// <param name="result"></param>
        [HttpGet]
        [Route("long-running-result/{guid}/{result}")]
        [ApiExplorerSettings(IgnoreApi = true)]
        public void  RunLongResult(string guid,string result)
        {
            //the GUID we generated at the beggining of the process
            //is being passed back from the backend together with a result.
            //we use this guid to identify the data in the dictionary and 
            //relese the Wait by using apiModel.ResetEvent.Set(); command
            bool isGuidCorrect = Guid.TryParse(guid, out Guid operationId);

            if (isGuidCorrect)
            {
                if (dict.TryGetValue(operationId, out ApiModel apiModel))
                {
                    apiModel.ApiResult = result;
                    //release waiting event
                    apiModel.ResetEvent.Set();
                }
            }
        }
        /// <summary>
        /// Entry point of the example. Call long running process on the backend
        /// and wait till it is finished
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        [Route("long-running-op")]
        public async  Task<IActionResult> RunLongOperation()
        {
            //for the simplicity of the process we will use 
            //get operation of HttpClient object
            var client = _clientFactory.CreateClient();
            //this will be unique identifier of the long running job
            Guid guid =  Guid.NewGuid();

            await client.GetStringAsync($"http://localhost:5010/BackendAPi/run-backend-op/{guid}");
           
            //add informaton about the job we are waiting for to the dictionary
            //we use ManulResetEvent that will wait for the signal to process the job
            ManualResetEvent mre = new ManualResetEvent(false);
            dict.TryAdd(guid, new ApiModel()
            {
                ResetEvent = mre
            }) ;
            //if no response after 10 seconds - continue anyway
            mre.WaitOne(1000 * 10);

            //handle the response
            if (dict.TryRemove(guid, out ApiModel apiModel))
            {
                if (!string.IsNullOrEmpty(apiModel.ApiResult))
                    return Ok(apiModel.ApiResult);
                else
                    return BadRequest("No result");
            }
            else
            {
                return BadRequest("Missing model object");
            }
            
        }
    }

Backend API


  /// <summary>
    /// Controller that simulates long running operation
    /// </summary>
    [ApiController]
    [Route("[controller]")]
    public class BackendAPi : ControllerBase
    {
        private static Random rnd = new Random();
        private readonly IHttpClientFactory _clientFactory;
        private readonly ILogger<BackendAPi> _logger;

        public BackendAPi(ILogger<BackendAPi> logger,
                          IHttpClientFactory clientFactory)
        {
            _logger = logger;
            _clientFactory = clientFactory;
        }

        /// <summary>
        /// Out long running operation is very simple
        /// we just wait for 5 seconds and after this 
        /// send the randon number back to the frontend API
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        [HttpGet]
        [Route("run-backend-op/{token}")]
        public IActionResult RunBackendOp( string token)
        {
            //simulation of the long running task 
            Task.Run(async ()=>
            {
                await Task.Delay(1000 * 5);
                var client = _clientFactory.CreateClient();
                var res = $"Result of long running {rnd.Next()}";
                await client.GetStringAsync($"http://localhost:5020/FrontendApi/long-running-result/{token}/{res}");
            });
            //note that the call from the fronend returned immediatelly, so frontend
            //can process another requests, but the actually result will be returned a little bit 
            //later
            return Ok("I got the token and started long running operation");
        }
    }

I also added swagger UI so you don't need to use Postman in order to run the test


Don't run the project on IIS. Start it on Kestrel


In the next part, I will explain the second option (asynchronous call). 

No comments:

Post a Comment