2
\$\begingroup\$

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);
\$\endgroup\$
  • \$\begingroup\$ Is the other side of the connection synchronous or async? If all the request are going into one connection and that connection is synchronous it may not matter what you do on your side of the connection. \$\endgroup\$ – pacmaninbw Aug 2 '18 at 15:37
  • \$\begingroup\$ @pacmaninbw Oh wow, that's a good question. I don't really have an answer for that. \$\endgroup\$ – Waragi Aug 2 '18 at 15:43
  • \$\begingroup\$ @pacmaninbw I submitted a question to the API developer. I guess we shall see. \$\endgroup\$ – Waragi Aug 2 '18 at 16:13
  • \$\begingroup\$ @pacmaninbw I am told the end point can support multiple simultaneous connections. \$\endgroup\$ – Waragi Aug 2 '18 at 17:06
  • 3
    \$\begingroup\$ To me this question looks rather off-topic since it's looking for a specific parallel implementation. This code also does not look like it's thread-safe because client = new RestClient("https://pproqa.xxxxxxxxx.com"); is being created in a loop and stored as field of the class. It'll be overwritten if called by multiple threads. Also if this is creating a HttpClient internally you might run out of sockets if you fire too many requests. \$\endgroup\$ – t3chb0t Aug 2 '18 at 17:14
4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ thank you so much, you taught me something new today! Much appreciated \$\endgroup\$ – Waragi Aug 3 '18 at 14:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.