I was having concurrency issues with my ASP.NET Web API 2 project with .NET Framework 4.6.2. (EF 6), although it has been live on production that has been running for more than 1 year, this problem has just arrived from a customer. I sent two concurrent requests to this specific endpoint from Postman, 2 records need to be updated but only one record is updated in the database.
The service I'm having this issue:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
// Code omitted for brevity.
List<GameBank> gameBankResult = null;
//Query GameBank database
gameBankResult = _unitOfWork.GameBankRepository.GetGames(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
if (gameBankResult != null && gameBankResult.Count() >= requestDto.quantity)
{
var k = requestDto.quantity - 1;
for (var i = k; i >= 0; --i)
{
gameBankResult[i].clientTrxRef = gameRequest.clientTrxRef;
gameBankResult[i].referenceId = gameRequest.referenceId;
gameBankResult[i].requestDateTime = DateTime.Now;
gameBankResult[i].responseDateTime = DateTime.Now;
}
//***** UPDATE GameBank *****
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
if (requestDto.quantity == 1)
{
//Code omitted for brevity.
}
}
_unitOfWork.Save();
return response;
}
I tried many things to successfully support concurrent requests and used WebApiThrottle
but It was not right to restrict online customers. I asked this in this post with the support of helpful software developers I came up with this solution.
I tested this with JMeter (10 threads, ramp-up period: 1 sec, 3 loops). I got no errors and I got 30 records updated in the database as well.
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
HttpResponseMessage response = null;
//ProductCode Conversion
var productCode =
_unitOfWork.ProductCodeRepository.GetByCode(p => p.clientCode == requestDto.productCode);
if (productCode != null)
{
requestDto.productCode = productCode.gameCode;
}
var gameRequest = _mapper.Map<RequestDto, GameRequest>(requestDto);
//Unique reference ID
gameRequest.referenceId = Guid.NewGuid();
var gameRequestDto = _mapper.Map<GameRequest, GameRequestDto>(gameRequest);
//Create signature
gameRequest = UtilitiesWatson.CreateSignature(gameRequestDto, RequestType.Initiate);
//Set service
gameRequest.service = "OUR";
gameRequest.customerID = 5; //WATSON
gameRequest.clientTrxRef = requestDto.clientTrxRef; //WATSON
//Add initiation request into database
_unitOfWork.GameRepository.Insert(gameRequest);
_unitOfWork.Save();
GameBank gameBankResult = null;
while (true)
{
try
{
gameBankResult = _unitOfWork.GameBankRepository.GetGame(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
_unitOfWork.Save();
if (gameBankResult != null)
{
gameBankResult.clientTrxRef = gameRequest.clientTrxRef;
gameBankResult.referenceId = gameRequest.referenceId;
gameBankResult.requestDateTime = DateTime.Now;
gameBankResult.responseDateTime = DateTime.Now;
_unitOfWork.GameBankRepository.Update(gameBankResult);
_unitOfWork.Save();
break; //exit from while loop
}
}
catch (DbUpdateConcurrencyException)
{
_unitOfWork.ClearChangeTracker(); //IS REQUIRED, so the next select will read new RowVersion also
Thread.Sleep((new Random()).Next(0, 1000)); //if you want to add a random pause 0-1 second
}
}
var gameBankConfirmResponse =
_mapper.Map<GameBank, GameConfirmResponse>(gameBankResult);
gameBankConfirmResponse.purchaseStatusDate = DateTime.Now;
gameBankConfirmResponse.clientTrxRef = gameRequest.clientTrxRef;
//ProductCode Conversion
var productCodeReverse = _unitOfWork.ProductCodeRepository.GetByCode(p =>
p.gameCode == requestDto.productCode);
if (productCodeReverse != null)
{
gameBankConfirmResponse.productCode = productCodeReverse.clientCode;
}
var resultResponse = JsonConvert.SerializeObject(gameBankConfirmResponse,
Formatting.Indented,
new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
response = new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.OK,
Content = new StringContent(resultResponse, System.Text.Encoding.UTF8,
"application/json"),
};
//Set service
gameBankConfirmResponse.service = "OUR";
gameBankConfirmResponse.clientTrxRef = requestDto.clientTrxRef;
_unitOfWork.GameConfirmResponseRepository.Insert(gameBankConfirmResponse);
_unitOfWork.Save();
return response;
}
Here is my ClearChangeTracker
in the unit of work. (Does this affect other operations?)
public void ClearChangeTracker()
{
foreach (var entry in _context.ChangeTracker.Entries())
{
entry.State = EntityState.Detached;
}
}
Can you share if there are issues that I can improve or that may be a problem? I'm curious about your comments.
Thank you.