Async await multiple API calls

Posted on

Problem

I have an application that needs to contact a remote API many times to request information about various products. It was taking around a minute with synchronous programming. I adjusted all my methods to be async and I still do not see a performance enhancement. I suspect it’s because I’m awaiting each result from the API instead of running them all in parallel.

The code works but it just does not work well. I’m really looking to learn and understand how to develop async methods the correct way.

Here is my API wrapper (if you guys have any suggestions for this too I’d love to hear it):

public class InventoryAPI
{
    private RestClient _client;
    private RestRequest request;

    public InventoryAPI(RestClient client)
    {
        this._client = client;
    }

    public async Task<RootLotsObject>  GetInventory(string company, string buyer, string vendCode, string lotNum, string prodNum, string status)
    {
        request = new RestRequest("ppro/services", Method.POST);
        var cancelTokenSource = new CancellationTokenSource();

        request.AddParameter("appId", "APIINV");
        request.AddParameter("command", "getInventory");
        request.AddParameter("username", "jraynor");
        if (company != null) { request.AddParameter("company", company); }
        if (buyer != null) { request.AddParameter("buyer", buyer); }
        if (vendCode != null) { request.AddParameter("vendor", vendCode); }
        if (lotNum != null) { request.AddParameter("lotNum", lotNum); }
        if (prodNum != null) { request.AddParameter("prodNum", prodNum); }
        if (status != null) { request.AddParameter("status", status); }

        IRestResponse<RootLotsObject> response = await client.ExecuteTaskAsync<RootLotsObject>(request, cancelTokenSource.Token);
        request = null;
        return response.Data;
    }
}

Here I call the API wrapper:

public LoadSchedulingRepository(RestClient client)
        {
            db = new LoadEntities();
            invAPI = new InventoryAPI(client);
        }
public async Task<IEnumerable<InventoryProdAndSubsVM>> GetInventoryForOrdersAPIAsync(string prodNum, DateTime? userDate)
        {
            var invResults = await  invAPI.GetInventory("0009", null, null, null, prodNum, "O");

            if (invResults != null)
            {
                var results = invResults.responseData.lots.Where(x => x.ExcludeFIFOAlloc == "N").GroupBy(l => l.productNum).Select(x => new InventoryProdAndSubsVM
                {
                    ProdDesc = x.Max(y => y.productDesc),
                    ProdCode = x.Max(y=>y.product),
                    ProdNum = x.Key,
                    Committed = invResults.responseData.lots.Where(w => w.ReceiveDate != null).Sum(z => Convert.ToDecimal(z.commitQty)),
                    DueIn = invResults.responseData.lots.Where(w => w.ExpectedDate == userDate).Sum(z => Convert.ToDecimal(z.orderedQty)),
                    OnHand = x.Sum(y => y.OnHand),
                    Condition = x.Max(y => y.condition),
                    Commodity = x.Max(y => y.commodity)
                });
                return results;
            }
            return null;
        }

Here is my bottle neck method I believe:

private async Task<List<InventoryProdAndSubsVM>> GetProductInventoryAsync(IEnumerable<BackhaulTopVM> prodCodes, DateTime userDate)
        {

            //Only take unique ProdCodes
            foreach (var product in prodCodes.GroupBy(x => x.ProdNum).Select(x => x.FirstOrDefault()))
            {
                //Get inventory for specific product code and append it to a list
                //BOTTLE NECK HERE;

                itemsToAdd = await loadSchedulingRepo.GetInventoryForOrdersAPIAsync(product.ProdNum, userDate);
                foreach (var item in itemsToAdd)
                {
                    //Create a new list item and add it to resultSet
                    var currentInventory = new InventoryProdAndSubsVM
                    {
                        Commodity = item.Commodity,
                        DueIn = item.DueIn,
                        OnHand = item.OnHand,
                        Committed = item.Committed,
                        ProdCode = item.ProdCode,
                        ProdNum = item.ProdNum,
                        ProdDesc = item.ProdDesc
                    };
                    //Append to initalized list
                    resultSet.Add(currentInventory);
                }
            }

            return resultSet;
        }

Here I’m awaiting loadSchedulingRepo.GetInventoryForOrdersAPIAsync
and I think it needs to complete a web request for every single ProdCode in prodCodes.

Is there a way to run every rest request in parallel or something?

I was toying around with this:

var prodCodeTasks = prodCodes.Select(x => loadSchedulingRepo.GetInventoryForOrdersAPIAsync(x.ProdNum, userDate)).ToList();
            await Task.WhenAll(prodCodeTasks);

Solution

The idea about using Task.WhenAll is a good start as it could help with running then in parallel.

Take a look at the following refactor

private async Task<List<InventoryProdAndSubsVM>> GetProductInventoryAsync(IEnumerable<BackhaulTopVM> prodCodes, DateTime userDate) {
    //Only take unique ProdCodes
    var uniqueProductCodes = prodCodes.GroupBy(code => code.ProdNum).Select(g => g.FirstOrDefault())
    //Create tasks to get inventory for the product codes.
    var prodCodeTasks = uniqueProductCodes.Select(product =>
        loadSchedulingRepo.GetInventoryForOrdersAPIAsync(product.ProdNum, userDate)
    );
    //wait for all the tasks to complete
    var inventory = await Task.WhenAll(prodCodeTasks);
    //grab all items returned from the async tasks
    var resultSet = inventory.SelectMany(items =>
        items.Select(item =>  new InventoryProdAndSubsVM 
        {
            Commodity = item.Commodity,
            DueIn = item.DueIn,
            OnHand = item.OnHand,
            Committed = item.Committed,
            ProdCode = item.ProdCode,
            ProdNum = item.ProdNum,
            ProdDesc = item.ProdDesc
        })
    ).ToList();

    return resultSet;
}

Initially you were still making each request in sequence even though you were doing it asynchronously. The speed would be the similar to what was happening before, with the only difference being that it probably was not locking the main thread.

With the above approach the rest calls all happen at the same time with the overall duration being as long as the longest request.

Leave a Reply

Your email address will not be published. Required fields are marked *