Slow down execution in Funcion

Web app is waiting for the quoteNum to be returned from the first call to it in order to associate its record with the ERP record. The second function sends some additional data to the web app with the quote num and additional info to further update it. Because the of the problem above, the second function actually executes its HTTP request to the web app prior to the first function returning its HTTP response (which includes the quotenum) to the web app.

You might say I should just return the data in function 2 with function 1, and I’d agree, but we are trying to avoid that if possible.

Can the webapp make a synchronous call to the first function and then make an async call to the second function once you get the quote number?

How is the second function being called in Epicor from the first one? You are for sure experiencing Async calls. A Thread wait is not a good solution. It requires the stars to align for consistent results.

Ideally the second function would be in the callback success of the first function from the webapp. For the most part all modern web apps will be 100% async. You have to use callbacks to fake a sync call. Doesn’t sound like Aaron has much control over that aspect though.

1 Like

You’re absolutely right. That would be the best way to do that.

1 Like

I decided to sell the idea of returning it in the response instead of the nonsense hoops I was jumping through to do it the other way.
so instead of just retuning quoteNum, I’ll return everything

{
    "quoteNum": 70285,
    "regionOut": "East",
    "distOut": "Southern Edge Orthopedics"
}

Then they can figure out how to parse some very simple json :wink:

Functions execute synchronously I believe, not much control over those. My Web API executes the call to the function async as seen in my controller below:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using EpicorBridge.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace EpicorBridge.Controllers
{
    [ApiVersion("1")]
    [Route("api/v{version:apiVersion}/[controller]/[action]")]
    [ApiController]
    [ApiKeyAuth]
    [Produces("application/json")]
    public class OrderController : ControllerBase
    {
        private readonly EpiAPIConnect _epiAPIConnect;
        public OrderController(EpiAPIConnect epiAPIConnect)
        {
            _epiAPIConnect = epiAPIConnect ?? throw new ArgumentNullException(nameof(epiAPIConnect));
        }
        
        /// <summary>
        /// Creates either a Sales Order or a Quote depending on the query parameter orderType being equal to fresh or tendon
        /// The body contains the order data that is used to create the order or quote.
        /// </summary>
        /// <param name="fxRequest"></param>
        /// <param name="OrderType">fresh|tendon</param>
        /// <remarks>
        /// Example Order Schema (Tendons)
        /// 
        ///     POST /CreateOrder 
        ///     {
        ///         "soldToCustNum":7278,
        ///         "billToCustNum":7278,
        ///         "shipToNum":"DEFAULT",
        ///         "poNum":"123456",
        ///         "shipMethod: "FDX 1st Overnight",
        ///         "needByDate":"2020-10-25",
        ///         "productList":"[\r\n{\"PartNum\": \"SPD-001\", \"LotNum\": \"201161010\"},\r\n{\"PartNum\":\"WPL-002\",\"LotNum\": \"171269023\"}\r\n]",
        ///         "repPerConID": 3488
        ///     }
        ///    
        /// Example Quote Schema (Fresh/Meniscus)
        /// 
        ///     POST /CreateOrder
        ///     {
        ///         "soldToCustNum":8145,
        ///         "billToCustNum":8145,
        ///         "shipToNum":"",
        ///         "poNum":"123456",
        ///         "needByDate":"2020-10-25",
        ///         "ptName":"Test Name",
        ///         "ptHeight":"Test Height",
        ///         "ptWeight":1000,
        ///         "ptDefect":"Test Defect Note",
        ///         "ptGender":"Male",
        ///         "procedure":"OATS",
        ///         "productList": "[\r\n{\"PartNum\": \"32247001\"},\r\n{\"PartNum\":\"45647010\"}\r\n]",
        ///         "ptAge":25,
        ///         "repPerConID": 3488
        ///     }
        /// </remarks>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> CreateOrder([FromBody] dynamic fxRequest, [Required]string OrderType = "")
        {
            if(OrderType.ToLower()==("fresh"))
            {
                var response = await _epiAPIConnect.InvokeFunction(EpiFunctions.EpicorBridge, EpiFunctions.CreateQuote, fxRequest);
                return response;
            }
            if (OrderType.ToLower()==("tendon"))
            {
                var response = await _epiAPIConnect.InvokeFunction(EpiFunctions.EpicorBridge, EpiFunctions.CreateSalesOrder, fxRequest);
                return response;
            }
            else return BadRequest();
        }

        /// <summary>
        /// Updates the Quote Status (QuoteHed.ActiveTaskID)
        /// </summary>
        /// <param name="fxRequest"></param>
        /// <remarks>        
        /// Move to "Hold" allowed if the quote in Epicor is in Active status
        /// 
        /// Move to "Active" allowed if quote in Epicor is on Hold status
        /// 
        /// Move to "Cancelled" status allowed at any point
        /// 
        /// No Status change allowed if the quote has an Allocation
        /// 
        /// Example Move to Active
        /// 
        ///     POST/UpdateQuoteStatus
        ///     {
        ///         quoteNum : 618012,
        ///         newTask : "ACTV"
        ///     }
        ///     
        /// Example Move to Hold   
        /// 
        ///     POST/UpdateQuoteStatus
        ///     {
        ///         quoteNum : 618012,
        ///         newTask : "HOLD"
        ///     }
        ///     
        /// Example Move to Hold   
        /// 
        ///     POST/UpdateQuoteStatus
        ///     {
        ///         quoteNum : 618012,
        ///         newTask : "CANCL"
        ///     }
        ///     
        /// </remarks>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> UpdateQuoteStatus([FromBody] dynamic fxRequest)
        {
            var response = await _epiAPIConnect.InvokeFunction(EpiFunctions.EpicorBridge, EpiFunctions.UpdateQuoteStatus, fxRequest);
            return response;            
        }

        /// <summary>
        /// Updates a given Quote or Sales Order PO Num field with the given inputs
        /// </summary>
        /// <param name="fxRequest"></param>
        /// <param name="OrderType">fresh|tendon</param>
        /// <remarks>
        /// This will overwrite the existing value in the PO Num field
        /// 
        /// The orderNum variable will be either the Sales Order num if tendon (pass with query param "tendon"), or a Quote Num for fresh/meniscus (pass with query param "fresh")
        /// 
        /// Example Update PO Num
        /// 
        ///     POST/UpdatePONum
        ///     {
        ///         "orderNum":200111,
        ///         "newPONum":"NewPOGoesHere"
        ///     }
        ///     
        /// </remarks>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> UpdatePONum([FromBody] dynamic fxRequest, [Required] string OrderType = "")
        {
            if (OrderType.ToLower() == ("fresh"))
            {
                var response = await _epiAPIConnect.InvokeFunction(EpiFunctions.EpicorBridge, EpiFunctions.UpdatePONumQuote, fxRequest);
                return response;
            }
            if (OrderType.ToLower() == ("tendon"))
            {
                var response = await _epiAPIConnect.InvokeFunction(EpiFunctions.EpicorBridge, EpiFunctions.UpdatePONumSO, fxRequest);
                return response;
            }
            else return BadRequest();
        }
    }
}

That controller is injected with a service that calls Epicor and passes the contents of the call to the controller into the Epicor function:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using RestSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace EpicorBridge.Utils
{
    public class EpiAPIConnect: ControllerBase
    {
        private readonly IOptions<EpiSettings> _epiSettings;
        private readonly EpiUtils _epiUtils;
        
        private readonly string _apiKey;
        private readonly string _user;
        private readonly string _path;
        private readonly string _fxPath;
        private readonly string _baqPath;
        private readonly string _licenseType;

        public EpiAPIConnect(IOptions<EpiSettings> app, EpiUtils utils)
        {
            _epiSettings = app ?? throw new ArgumentNullException(nameof(app));
            _epiUtils = utils ?? throw new ArgumentNullException(nameof(utils));

            _apiKey = $"{_epiSettings.Value.ApiKey}";
            //_path = $"{_epiSettings.Value.Host}/{_epiSettings.Value.Instance}/api/v2/";
            _path = $"{_epiSettings.Value.Host}/{_epiSettings.Value.Instance}/api/v2/odata/{_epiSettings.Value.Company}";
            _fxPath = $"{_epiSettings.Value.Host}/{_epiSettings.Value.Instance}/api/v2/efx/{_epiSettings.Value.Company}";
            _baqPath = $"{_epiSettings.Value.Host}/{_epiSettings.Value.Instance}/api/v2/odata/{_epiSettings.Value.Company}";
            _user = $"{_epiSettings.Value.IntegrationUser}:{_epiSettings.Value.IntegrationPassword}";
            _licenseType = $"{_epiSettings.Value.LicenseTypeGuid}";
        }

    
        /// <summary>
        /// Invokes an Epicor Function from the specific function library
        /// </summary>
        /// <param name="library">The Library ID associated with the function</param>
        /// <param name="functionID">The Function ID to be invoked</param>
        /// <param name="fxRequest">The JSON from the call body representing the optional input parameters</param>
        /// <returns></returns>
        public async Task<IActionResult> InvokeFunction(string library, string functionID, dynamic fxRequest)
        {
            if (_epiUtils.ValidSession(_epiUtils.sessionID, _licenseType, _path, _user, _apiKey, out string msg))
            {
                var restClient = new RestClient(_fxPath)
                {
                    RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
                };
                var request = new RestRequest($"{library}/{functionID}", Method.POST);
                //add any optional request parameters
                request.AddParameter("application/json", fxRequest, ParameterType.RequestBody);

                //Web Service License
                var headerLicense = new
                {
                    ClaimedLicense = _licenseType,
                    SessionID = _epiUtils.sessionID
                };
                var header = JsonConvert.SerializeObject(headerLicense);
                request.AddHeader("License", header);
                request.AddHeader("Authorization", $"Basic {EpiUtils.Base64Encode(_user)}");
                request.AddHeader("x-api-key", _apiKey);

                IRestResponse response = await restClient.ExecuteAsync(request);
                switch (response.StatusCode)
                {
                    case System.Net.HttpStatusCode.BadRequest:
                        {
                            dynamic content = JsonConvert.DeserializeObject(response.Content);
                            var value = content;
                            return BadRequest(content);
                        }
                    case System.Net.HttpStatusCode.OK:
                    default:
                        {
                            dynamic content = JsonConvert.DeserializeObject(response.Content);
                            var value = content;
                            return Ok(content);
                        }
                }
            }
            else
            {
                return Unauthorized(msg);
            }
        }

        /// <summary>
        /// Executes a Paramaterized BAQ 
        /// </summary>
        /// <param name="BAQID">BAQ ID</param>
        /// <param name="query">Query strings passed into the call</param>
        /// <param name="verb">RestSharp Method</param>
        /// <returns></returns>
        public async Task<IActionResult> ExecuteBAQ(string BAQID, IQueryCollection query , Method verb)
        {
            if (_epiUtils.ValidSession(_epiUtils.sessionID, _licenseType, _path, _user, _apiKey, out string msg))
            {
                var restClient = new RestClient(_baqPath)
                {
                    RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
                };
                var request = new RestRequest($"BaqSvc/{BAQID}/Data", verb);

                //add any optional request parameters, excluding API key of calling app              
                foreach (var p in query)
                {
                    if (p.Key != "api_key")
                    {
                        request.Parameters.Add(new RestSharp.Parameter(p.Key, p.Value, RestSharp.ParameterType.QueryString));
                    }
                }

                //License type as defined in config
                var headerLicense = new
                {
                    ClaimedLicense = _licenseType,
                    SessionID = _epiUtils.sessionID
                };
                var header = JsonConvert.SerializeObject(headerLicense);
                request.AddHeader("License", header);
                request.AddHeader("Authorization", $"Basic {EpiUtils.Base64Encode(_user)}");
                request.AddHeader("x-api-key", _apiKey);

                IRestResponse response = await restClient.ExecuteAsync(request);
                switch (response.StatusCode)
                {
                    case System.Net.HttpStatusCode.BadRequest:
                        {
                            dynamic content = JsonConvert.DeserializeObject(response.Content);
                            var value = content.value;
                            return BadRequest(value);
                        }
                    case System.Net.HttpStatusCode.OK:
                    default:
                        {
                            //Trim down the Epcior response to remove the metadata node and return only the value
                            dynamic content = JsonConvert.DeserializeObject(response.Content);                            
                            var value = content.value;
                            return Ok(value);
                        }
                }
            }
            else
            {
                return Unauthorized(msg);
            }
        }

    }
}

So yes, agreed on the async for web stuff. I think the issue was in fact that the initial HTTP call to the function is all on the same thread and any subsequent actions in Epicor are also on that thread.

1 Like

Your CreateOrder and UpdateQuoteStatus are independent Async calls. What block are those two called in? If the later is not called in the callback from the former that is where you have your issue.

UpdateQuoteStatus is a totally separate call, not associated with this problem :slight_smile:

So where and how is your second function getting called and how is it updating the web app if it wasn’t included in the original return data and isn’t being called from the web app?

In the first function, I was using an “invoke function” widget to invoke function 2 inside function 1. Function 2, when invoked, also performs an HTTP call. Function 2 is not always invoked by function 1, but in this case when I was invoking it from function 1, it paused the HTTP response from function 1 until after the HTTP response in function 2 had executed

I suck at formatting my text but this is the flow I saw with the invoke function 2 from function 1 and the evidence that it was having function 2 execute its code/HTTP request prior to the function 1 returning its response. Also numbering lists is apparently outside my area of expertise :woozy_face:

The assumption I made before was that Function 1 would execute and return its response to the caller after the code had finished executing. By invoking another function, which seems to be only able to be done synchronously, it waits for the invoked function 2 to execute prior to finalizing the response from function 1 back to the caller. In this way, we have called the web app to update a record that it doesn’t know exists yet because it’s relying on the response from function 1 to tell it the record number.

Solving it with returning the data in 1 call instead of this mess is the right approach, I just wanted to see if we could do it this way. Apparently not!