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:
The static variables
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.
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.
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.
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.
ASP.NET forums are moving to a new home on Microsoft Q&A, we encourage you to go to Microsoft Q&A for .NET for posting new questions and get involved today. Learn more >
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.
Member
24 Points
174 Posts
Questions on threading, async, await in web api
Apr 10, 2019 10:22 AM|AnyUserNameThatLetsMeIn|LINK
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:
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.
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! :)
All-Star
53641 Points
24004 Posts
Re: Questions on threading, async, await in web api
Apr 10, 2019 11:54 AM|mgebhard|LINK
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.
Member
24 Points
174 Posts
Re: Questions on threading, async, await in web api
Apr 10, 2019 12:08 PM|AnyUserNameThatLetsMeIn|LINK
When you put it like that I am very much inclined to agree with you. I will investigate further.
Star
11464 Points
2439 Posts
Re: Questions on threading, async, await in web api
Apr 11, 2019 06:41 AM|Yohann Lu|LINK
Hi friend,
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
All-Star
58444 Points
15769 Posts
Re: Questions on threading, async, await in web api
Apr 11, 2019 02:07 PM|bruce (sqlwork.com)|LINK