Background:
We have built a React SPA that communicates with a ASP.NET Web API 2 back end, .NET Framework 4.6.1
. On our initial load we are making two separate requests to load data. When loading a lot of data we noticed that the API requests were both considerably slower when we made them in the application than when we tried the requests individually in Postman.
Original:
Example structure, fetch uses our API of course:
fetch('http://example.com/movies.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(JSON.stringify(myJson));
});
fetch('http://example.com/otherMovies.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(JSON.stringify(myJson));
});
Example C# API method:
[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult GetRenewalCycleForCustomer(string bsid, int caseId)
{
var customerNumbers = GetCustomerNumbers();
var userId = HttpContext.Current.User.Identity.GetUserId<int>();
var user = identityDb.Users.Find(userId);
var customerNumbers = user.ApplicationUserCustomerNumbers.Select(x => new CustomerNumberKey() { bsid = x.CustomerNumber.bsid, NameNo = x.CustomerNumber.NameNo }).ToList();
var db = new DbContext();
var caseService = new CaseService(db);
var portfolioTabViewModel = caseService.GetPortfolioTabViewModelForCustomer(bsid, caseId, customerNumbers);
return Ok(portfolioTabViewModel);
}
OS is Windows 10 Pro and IIS should be able to handle 10 concurrent connections according to the Internet. Does not matter anyway because we host it on Azure as a Windows Server and we have the same respons times there. Tried App Service as well and it was the same result.
Testing the response times synchronously with Postman Runner
Two instances of Postman Runner going at the same time:
Other notes:
Does not seem to be hardware related since CPU, Memory and Disk are not affected noteworthy if requests are being made at the same time or not.
What can we do to fix this?
Non async method that seems to run in parallel:
https://stackoverflow.com/a/26737847/3850405
Some threads are suggesting session state but I can't find any reference to that in Web.config
and it is not anything that I have enabled. Searching for HttpContext.Current.SetSessionStateBehavior
gives 0 results.
https://stackoverflow.com/a/26172317/3850405
Windows 10 resources:
https://serverfault.com/a/800518/293367
https://forums.asp.net/t/2100558.aspx?concurrent+connections+on+windows+pro+10+IIS
Update Async and IIS:
It does not seem to be related to async or the IIS, tested concurrent requests with the methods below. The concurrent slow requests seems to depend on something else.
Async:
[HttpGet]
[Route("{bsid}/{caseId}")]
public async Task<IHttpActionResult> Get(string bsid, int caseId)
{
await Task.Delay(3000);
return Ok();
}
Sync:
[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult Get(string bsid, int caseId)
{
Thread.Sleep(3000);
return Ok();
}
Update 2: database with Async and sync call:
It does not seem to be the database call either. Testing with Include
the calls are similar in speed even though the async call is considerably slower.
Async:
[HttpGet]
[Route("{bsid}/{caseId}/async")]
public async Task<IHttpActionResult> GetAsync(string bsid, int caseId)
{
var db = new DbContext();
var deviations = await db.RenewalCycles.Where(x => x.Deviations.Any())
.Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPName))
.Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPNameType))
.Include(cycle => cycle.TPCase.GoodsAndServicesDescriptions)
.Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCaseRelation))
.Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCountry))
.ToListAsync();
return Ok();
}
Sync:
[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult Get(string bsid, int caseId)
{
var db = new DbContext();
var deviations = db.RenewalCycles.Where(x => x.Deviations.Any())
.Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPName))
.Include(cycle => cycle.TPCase.CaseNames.Select(caseName => caseName.TPNameType))
.Include(cycle => cycle.TPCase.GoodsAndServicesDescriptions)
.Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCaseRelation))
.Include(cycle => cycle.TPCase.RelatedCases.Select(relatedCase => relatedCase.TPCountry))
.ToList();
return Ok();
}
The mayor overhead part turned out to be database related after all. A method used db.Cases.Find(bsid, caseId)
and then converted the model to a view model. The Cases
model in turn had a lot of relations and all of these where issuing a separate database call due to that the model had properties marked as virtual
, like this public virtual TPRenewalCycle TPRenewalCycle { get; set; }
to enable lazy loading. Found it by looking at Visual Studio Output Window (Debug -> Windows -> Output) and setting ApplicationDbContext like below.
public class ApplicationDbContext : DbContext
{
protected const string ConnectionStringName = "defaultConnection";
public ApplicationDbContext() : base(ConnectionStringName)
{
Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
Database.CommandTimeout = 300;
}