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 
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.