Search code examples
c#asp.netiisconcurrencyiis-10

Concurrent request in ASP.NET Web API 2 is nearly six times slower than a single request


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

enter image description here

Two instances of Postman Runner going at the same time:

enter image description here

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();
}

enter image description here

Sync:

[HttpGet]
[Route("{bsid}/{caseId}")]
public IHttpActionResult Get(string bsid, int caseId)
{
    Thread.Sleep(3000);
    return Ok();
}

enter image description here

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();
}

enter image description here

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();
}

enter image description here


Solution

  • 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;
        }