locked
Questions on threading, async, await in web api RRS feed

  • Question

  • User-1458727574 posted

    Hopefully this time I am posting the right forum. I am developing an ASP.NET Core Web API. I have a problem with responses being corrupted and I think it is because, in short, I'm doing it wrong. I've been learning this over the last year and been reading tonnes of documentation and getting help when things aren't working correctly. So, here I am again asking for help.

    I have a controller that accepts an HTTP post that takes an XML doc in the request header. It processes the inbound XML and sends data to a Sage system via Sage's web API. It also spawns two external processes to do some specific jobs that cannot be done via Sage's web API. These are Windows executables that accept input data via command line parameters and return back to the API using their respective exit codes. Once completed, the API returns a response back to the user as an XML string returned in the BODY of the response rather than the header. I will post the whole controller without the actual processing of the XML data and what it does with it as there are over 1000 lines of code that aren't relevant to this post, but the internal class functions will be there. I feel that the problem is a few things:

    1. The static variables
    2. Threading isn't working as I'd hoped

    So, please don't condemn the code as I have had this dumped on me to produce a web API when I have never done such a thing before.

    // Bunch of using directives here that aren't necessary for the post
    
    namespace ACLWebApi.Controllers
    {
        [Route("api/[controller]")]
        [ApiController]
        public class BillingsController : ControllerBase
        {
            private static IConfiguration appConfiguration;
            private static ACLSettings aCLSettings = new ACLSettings();
            private static BillsBill bill;
            private Bills bills;
            private static string responsePayload = "";
            private static string sageCompanyName = "";
            private static ILogger<BillingsController> aclLogger;
            private static IMemoryCache glMappingsCache;
    
            public BillingsController(IConfiguration Configuration, ILogger<BillingsController> logger, IMemoryCache cache)
            {
                appConfiguration = Configuration;
                aCLSettings = appConfiguration.GetSection("ACLSettings").Get<ACLSettings>();
                aclLogger = logger;
                glMappingsCache = cache;
            }
    
            private string XMLToString(Bills bills)
            {
                // Outputs the bills object to a string to return to the client
            }
    
            private static string XMLBillingDataToString(billingDataEntity billingData)
            {
                // Outputs the input XML in the request header as a string for logging purposes
            }
    
            // POST api/<controller>
            [HttpPost("")]
            public async Task<string> Post([FromBody] billingDataEntity billingData, [FromQuery] string s = "")
            {
                string Sage300WebAPIURI = ((aCLSettings.UseHTTPS == "Y") ? "https://" : "http://") +
                                            aCLSettings.SageWebApiHostName + "/" +
                                            aCLSettings.SageWebApiEndpointName + "/v" +
                                            aCLSettings.Sage300WebApiVersion + "/-/";
    
                aclLogger.Log(LogLevel.Information, "Begin transaction in Billing Controller");
                bill = new BillsBill();
                bills = new Bills();
                LogLevel logLevel = new LogLevel();
    
                responsePayload = "";
                ResponsePayload = "";
                string output = "";
    
                if (!ModelState.IsValid)
                {
                    bill.Status = "error";
                    bill.ErrorId = "500";
                    bill.ErrorMessage = "Invalid XML";
                }
                else
                {
                    sageCompanyName = s;
                    aclLogger.Log(LogLevel.Information, "Sage company: {0}", sageCompanyName);
    
                    if (sageCompanyName != "")
                    {
                        await ProcessBillingFile(Sage300WebAPIURI + sageCompanyName + "/", billingData);
                    }
                    else
                    {
                        bill.Status = "error";
                        bill.ErrorId = "500";
                        bill.ErrorMessage = "Sage Company not specified in query string";
                    }
                }
    
                if(!string.IsNullOrEmpty(bill.ErrorId))
                {
                    bill.Status = "error";
                    logLevel = LogLevel.Error;
                }
                else
                {
                    logLevel = LogLevel.Information;
                }
    
                bills.Bill.Add(bill);
                output = XMLToString(bills);
                aclLogger.Log(logLevel, output);
    
                bill.ErrorId = "";
                bill.ErrorMessage = "";
                bill.Fileno = "";
                bill.FileSuffix = "";
                bill.Status = "";
                bills.Bill.Clear();
    
                ResponsePayload = "";
                aclLogger.Log(LogLevel.Information, "End transaction in Billings Controller");
    
                return output;
            }
    
            public static string ResponsePayload { get => responsePayload; set => responsePayload = value; }
    
            /// <summary>
            /// Creates billing entries
            /// </summary>
            public async Task ProcessBillingFile(string psURI, billingDataEntity billingData)
            {
                // This function is the main processing of the inbound XML. It deserialises the XML, makes numerous calls to the other functions in the class
                // At the end, it gets a file number and a suffix value and adds that to the return bills object
                
                
                // ProcessDutyForOptionalFields(ref billingData, ref dMPF, ref dHMF);
                // ProcessChargeCodesAndChargeTypesForARInvoice(ref billingData, ref bDutyChargesExist, ref dChargeCode99Value);
                                // gLAccountsGetARRevenue = JsonConvert.DeserializeObject<GLAccountsGet>(JsonConvert.SerializeObject(await SendRequest(new HttpMethod(sHTTPVerb), sFullURI + "?%24select=UnformattedAccount%2CDescription%2CAccountType%2CStatus%2CAccountNumber", "GetGLAcc")));
                                    // aRDistributionCodes = JsonConvert.DeserializeObject<ARDistributionCodes>(JsonConvert.SerializeObject(await SendRequest(new HttpMethod("GET"), psURI + @"AR/ARDistributionCodes('" + sChargeCode + "')", "GetDistributionCodes")));
                                // gLAccountsGetAPExpense = JsonConvert.DeserializeObject<GLAccountsGet>(JsonConvert.SerializeObject(await SendRequest(new HttpMethod(sHTTPVerb), sFullURI + "?%24select=UnformattedAccount%2CDescription%2CAccountType%2CStatus%2CAccountNumber", "GetGLAcc")));
                                    // aPDistributionCodes = JsonConvert.DeserializeObject<APDistributionCodes>(JsonConvert.SerializeObject(await SendRequest(new HttpMethod("GET"), psURI + @"AP/APDistributionCodes('" + sChargeCode + "')", "GetDistributionCodes")));
                    // dynamic sARBatch = await SendRequest(new HttpMethod(sHTTPVerb), sFullURI, "InsertARBatch", aRInvoiceBatch);
                            // await RunPostReadinessAndPost(sARBatchNumber, "POST", psURI, "AR", "ARPostInvoices('$process')", "ARInvoiceBatches");
                    // dynamic sAPBatch = await SendRequest(new HttpMethod(sHTTPVerb), sFullURI, "InsertAPBatch", aPInvoiceBatch);
                            // await RunPostReadinessAndPost(sAPBatchNumber, "POST", psURI, "AP", "APPostInvoices('$process')", "APInvoiceBatches");
    
                // bill.Fileno = billingData.fileNo.ToString();
                // bill.FileSuffix = sInvoiceSuffix.Replace("-", "");
            }
    
            public static async Task RunPostReadinessAndPost(string psBatchNumber, string psHTTPVerb, string psURI, string psModule, string psPostEndpoint, string psReadinessEndpoint)
            {
                // This function calls an external Process() to do some processing in the external Sage system that cannot be done via Sage's web API
                // It returns an exit code which is used in a switch statement to determine what happened
                // If all is good, then it calls the SendRequest function again to finish off
                            await SendRequest(new HttpMethod(psHTTPVerb), sFullURI, "PostInvoice", aRPostInvoices);
                            await SendRequest(new HttpMethod(psHTTPVerb), sFullURI, "PostInvoice", aPPostInvoices);
            }
    
            /// <summary>
            /// Sends a Sage 300 Web API request with a request payload and returns the object within the response payload
            /// </summary>
            /// <param name="method">The method representing the HTTP verb to request with</param>
            /// <param name="requestUri">The request Uri</param>
            /// <param name="payload">Optional, The payload to be sent to Sage</param>
            /// <returns></returns>
            public static async Task<object> SendRequest(HttpMethod method, string requestUri, string sActionType, object payload = null)
            {
                HttpContent content = null;
                string sageResponse = "";
                SageError sageError = new SageError();
    
                // Serialize the payload if one is present
                if (payload != null)
                {
                    var payloadString = JsonConvert.SerializeObject(payload, Formatting.Indented, new JsonSerializerSettings
                    {
                        NullValueHandling = NullValueHandling.Ignore,
                        DefaultValueHandling = DefaultValueHandling.Ignore
                    });
    
                    content = new StringContent(payloadString, Encoding.UTF8, "application/json");
                }
    
                // Create the Web API client with the appropriate authentication
                using (var httpClientHandler = new HttpClientHandler { Credentials = new NetworkCredential(aCLSettings.AccpacUserId.ToUpper(), aCLSettings.AccpacUserPassword.ToUpper()) })
                using (var httpClient = new HttpClient(httpClientHandler))
                {
                    // Create the Web API request
                    var request = new HttpRequestMessage(method, requestUri)
                    {
                        Content = content
                    };
    
                    // Send the Web API request
                    try
                    {
                        var response = await httpClient.SendAsync(request);
                        sageResponse = await response.Content.ReadAsStringAsync();
                        sageError = JsonConvert.DeserializeObject<SageError>(sageResponse);
    
                        int statusNumber = (int)response.StatusCode;
                        string statusMessage = response.ReasonPhrase;
    
                        if (sActionType == "GetDistributionCodes" || sActionType == "GetGLAcc")
                        {
                            if(statusNumber == 404)
                            {
                                aclLogger.Log(LogLevel.Error, "Error performing action {0}: {1}", sActionType, statusMessage);
                            }
    
                            return JsonConvert.DeserializeObject(sageResponse);
                        }
                        else
                        {
                            switch (statusNumber)
                            {
                                case 200:           // Ok, send back the payload
                                    break;
                                case 201:           // Record was created
                                    if (sActionType != "PostInvoice")
                                    {
                                        bill.Status = "success";
                                        //billsBill.ErrorId = "";
                                        //billsBill.ErrorMessage = "";
                                    }
                                    break;
                                case 204:           // Record was updated
                                    if (sActionType != "PostInvoice")
                                    {
                                        bill.Status = "success";
                                        //billsBill.ErrorId = "";
                                        //billsBill.ErrorMessage = "";
                                    }
                                    break;
                                case 404:           // Record not found
                                    if (sActionType != "PostInvoice")
                                    {
                                        bill.Status = "error";
                                        bill.ErrorId = statusNumber.ToString();
                                        bill.ErrorMessage += sageError.Error.Message.Value == "" ? sageError.Error.Code + "\n" : sageError.Error.Message.Value + "\n";
                                        aclLogger.Log(LogLevel.Error, sageError.Error.Message.Value == "" ? sageError.Error.Code : sageError.Error.Message.Value);
                                    }
                                    break;
                                case 409:           // Trying to insert when record exists. Conflict
                                    if (sActionType != "PostInvoice")
                                    {
                                        sageError.Error.Message.Value = sageError.Error.Message.Value == "" && sageError.Error.Code == "RecordNotFound" ? "Distribution Code not found" : sageError.Error.Message.Value;
                                        bill.Status = "error";
                                        bill.ErrorId = statusNumber.ToString();
                                        bill.ErrorMessage += sageError.Error.Message.Value == "" ? sageError.Error.Code + "\n" : sageError.Error.Message.Value + "\n";
                                        aclLogger.Log(LogLevel.Error, sageError.Error.Message.Value == "" ? sageError.Error.Code : sageError.Error.Message.Value);
                                    }
                                    break;
                                case 422:
                                    if (sActionType != "PostInvoice")
                                    {
                                        bill.Status = "error";
                                        bill.ErrorId = statusNumber.ToString();
                                        bill.ErrorMessage += sageError.Error.Message.Value == "" ? sageError.Error.Code + "\n" : sageError.Error.Message.Value + "\n";
                                        aclLogger.Log(LogLevel.Error, sageError.Error.Message.Value == "" ? sageError.Error.Code : sageError.Error.Message.Value);
                                    }
                                    break;
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        bill.Status = "error";
                        bill.ErrorId = "500";
                        bill.ErrorMessage = e.Message;
                    }
                }
    
                return string.IsNullOrWhiteSpace(sageResponse) ? null : JsonConvert.DeserializeObject(sageResponse);
            }
    
            public static void ProcessChargeCodesAndChargeTypesForARInvoice(ref billingDataEntity pbillingDataEntity, ref bool pbDutyChargesExist, ref decimal pdChargeCode99Value)
            {
                // Function to perform internal pre-processing
            }
    
            public static void ProcessDutyForOptionalFields(ref billingDataEntity pbillingDataEntity, ref decimal pdMPF, ref decimal pdHMF)
            {
                // Function to perform internal pre-processing
            }
        }
    }

    The main task ProcessBillingFile has all the processing code removed except for the bits where it makes calls to other functions. I left those in so you can see how I am making those calls. The SendRequest is where the posts happen to Sage's web API from my web API. What is happening is that if there is an inbound request that comes in before the previous request has sent a response, things are getting a screwed. Now, the static variables are part of the problem because their running data is shared amongst the objects so how do I deal with those? The second problem is that I feel that the main Post function should really spawn off a thread that does its own processing and then return back to the client, but if multiple requests come in, how do I send back the responses to each client request? I am just not familiar with coding this way as it is new to me. If you could show examples rather than direct me to MS docs or SO docs that would be great.

    Be gentle! :)

    Wednesday, April 10, 2019 10:22 AM

Answers

  • User475983607 posted

    A static variable is basically a pointer to a single memory location accessible to the entire application.  Static variable are good for objects that do not change during run time.  Things like read-only members or configuration.  However, variables and can be be reassigned a value.   This is dangerous in a multi-thread environment, like a web application, because static variables are NOT thread safe.  One thread change the variable while another is reading the variable.  Remember the static variable is located in a specific location in memory and there is only one.  That causes dirty reads.  To stop dirty reads from happening you have to serialize access the static variable.  It's like a gate.  Thread 1 closes the gate and uses the variable.  Thread 2 wants to use the variable but has to wait until Thread 1 opens the gate.  Threads will stack up at the gate until Thread 1 releases the hold.  Then Tread 2 can use the variable.

    https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphore?view=netframework-4.7.2

    Static variables are not an ideal design in an async application.  Every computer is internally asynchronous.  An example is a network card. A network card can operate on its own without the CPU and simply interrupts the CPU when it has something.  Async application take advantage of the asynchronous hardware and OS.  Rather than a thread waiting for a response from a HTTP request, it goes back into the thread pool and can be used for something else.  When the network card gets the response it interrupts the CPU and a new thread handles the HTTP response.

    Given symptoms and briefly looking at the source code, it appears you have dirty reads.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Wednesday, April 10, 2019 11:54 AM

All replies

  • User475983607 posted

    A static variable is basically a pointer to a single memory location accessible to the entire application.  Static variable are good for objects that do not change during run time.  Things like read-only members or configuration.  However, variables and can be be reassigned a value.   This is dangerous in a multi-thread environment, like a web application, because static variables are NOT thread safe.  One thread change the variable while another is reading the variable.  Remember the static variable is located in a specific location in memory and there is only one.  That causes dirty reads.  To stop dirty reads from happening you have to serialize access the static variable.  It's like a gate.  Thread 1 closes the gate and uses the variable.  Thread 2 wants to use the variable but has to wait until Thread 1 opens the gate.  Threads will stack up at the gate until Thread 1 releases the hold.  Then Tread 2 can use the variable.

    https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphore?view=netframework-4.7.2

    Static variables are not an ideal design in an async application.  Every computer is internally asynchronous.  An example is a network card. A network card can operate on its own without the CPU and simply interrupts the CPU when it has something.  Async application take advantage of the asynchronous hardware and OS.  Rather than a thread waiting for a response from a HTTP request, it goes back into the thread pool and can be used for something else.  When the network card gets the response it interrupts the CPU and a new thread handles the HTTP response.

    Given symptoms and briefly looking at the source code, it appears you have dirty reads.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Wednesday, April 10, 2019 11:54 AM
  • User-1458727574 posted

    When you put it like that I am very much inclined to agree with you. I will investigate further.

    Wednesday, April 10, 2019 12:08 PM
  • User36583972 posted


    Hi friend,

    Hopefully this time I am posting the right forum. I am developing an ASP.NET Core Web API. I have a problem with responses being corrupted and I think it is because, in short, I'm doing it wrong. I've been learning this over the last year and been reading tonnes of documentation and getting help when things aren't working correctly. So, here I am again asking for help.

    Your question is more related to the ASP.NET Core Web API. You need to go to the ASP.NET Core forum.

    When we use multithreading and access the shared resources, you can refer the following links.

    Multithreading in C#

    C# variable thread safety

    Best Regards

    Yong Lu

    Thursday, April 11, 2019 6:41 AM
  • User-474980206 posted
    You use of static’s is an issue. You can only use static variables for data that is shared and the same for all requests. This is because two request that happen at the same time, share the static data. You should also use locking if for some reason, the requests update the static data.

    Thursday, April 11, 2019 2:07 PM