asp.net-coreentity-framework-coremediatr

How to resolve threading issues with DbContext on .NET Core, EF Core


Got threads issue on EF Core 8.0, .NET 8, I got this issue how to overcome this?

Error:

A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.

internal sealed class CommandHandler : IRequestHandler<Command, string>
{
    private readonly ApplicationDbContext _dbContext;
    private readonly IValidator<Command> _validator;
    private readonly IWebHostEnvironment _host;

    public CommandHandler(ApplicationDbContext dbContext, IValidator<Command> validator, IWebHostEnvironment host)
    {
        _dbContext = dbContext;
        _validator = validator;
        _host = host;
    }

    public async Task<string> Handle(Command request, CancellationToken cancellationToken)
    {
        var validationResult = await _validator.ValidateAsync(request);

        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        using (var transaction = _dbContext.Database.BeginTransaction())
        {
            var multiplePayment = request.PaymentRequest;
            var _paymentImageId = new List<int?>();
            var _paymentId = 0;
            var _imageName = new List<string?>();

            try
            {
                foreach (var item in multiplePayment)
                {
                    var getUserId = _dbContext.Auths
                        .FirstOrDefaultAsync(x => x.UserId == item.CreateBy, cancellationToken);

                    var generatedId = "Pay-" + GenerateId.MakeId();

                    #region image upload
                    if (item.ListOfImageBase64 is null || item.ListOfImageBase64.Count == 0)
                    {
                        foreach (var imageBase64 in item.ListOfImageBase64)
                        {
                            var createdBy = await _dbContext.Auths
                                .FirstOrDefaultAsync(x => x.UserId == item.CreateBy, cancellationToken);

                            string image = await Base64ToImage.UploadImageAsync(imageBase64, "Payments", createdBy.Name, WebRootUtility.GetWwwRootPath(_host));

                            var img = new Image()
                            {
                                ImageId = GenerateId.MakeId(),
                                GeneratedId = generatedId,
                                ImageName = Path.GetFileName(image),
                                ImageUrl = image,
                                ImageSize = new FileInfo(image).Length.ToString(),
                                ImageType = ".png",
                                CreatedTime = DateTime.Now
                            };

                            _dbContext.Images.Add(img);

                            await _dbContext.SaveChangesAsync();

                            _imageName.Add(img.ImageName);

                            var paymentImage = new PaymentImage
                            {
                                ImageId = img.Id,
                                CreatedTime = DateTime.Now
                            };
                            _dbContext.PaymentImages.Add(paymentImage);

                            await _dbContext.SaveChangesAsync();

                            _paymentImageId.Add(paymentImage.Id);
                        }
                    }
                    #endregion image upload

                    #region Payment
                    var checkCreateBy = await _dbContext.Auths
                        .FirstOrDefaultAsync(x => x.UserId == item.CreateBy, cancellationToken);

                    var payment = new Payment
                    {
                        PaymentId = generatedId,
                        CreateBy = item.CreateBy,
                        CreateByName = checkCreateBy.Name,
                        Source = item.Source,
                        Amount = item.Amount,
                        Method = item.Method,
                        DepositOption = item.DepositOption,
                        Date = DateTime.Now,
                        ChequeNoOrTrackingNo = item.ChequeNoOrTrackingNo,
                        Branch = item.Branch,
                        RoutingNo = item.RoutingNo,
                        Status = "Pending",
                        AuthId = checkCreateBy.Id,
                        CreatedTime = DateTime.Now,
                    };

                    _dbContext.Payments.Add(payment);
                    await _dbContext.SaveChangesAsync();
                    _paymentId = payment.Id;
                    #endregion Payment

                    #region OrderProcessList
                    foreach (var orderId in item.OrderIds)
                    {
                        var orderProcess = await _dbContext.OrderProcesses
                            .FirstOrDefaultAsync(x => x.OrderProcessId == orderId, cancellationToken);

                        var orderProcessList = new OrderProcessList
                        {
                            OrderProcessGeneratedId = orderProcess.OrderProcessId,
                            OrderProcessId = orderProcess.Id,
                            PaymentId = _paymentId,
                            CreatedTime = DateTime.Now
                        };
                        _dbContext.OrderProcessLists.Add(orderProcessList);
                        await _dbContext.SaveChangesAsync();
                    }
                    #endregion OrderProcessList

                    #region OrderProcess
                    foreach (var orderId in item.OrderIds)
                    {
                        var orderProcess = await _dbContext.OrderProcesses
                            .FirstOrDefaultAsync(x => x.OrderProcessId == orderId, cancellationToken);

                        var paymentAmount = await _dbContext.Payments
                            .Where(x => x.Id == _paymentId)
                            .OrderByDescending(x => x.Id)
                            .Select(x => x.Amount)
                            .FirstOrDefaultAsync(cancellationToken);

                        var totalAmount = decimal.Parse(orderProcess.TotalAmount) + decimal.Parse(paymentAmount);

                        var isPartial = decimal.Parse(orderProcess.TotalAmount) >= totalAmount ? false : true;

                        if (isPartial)
                        {
                            orderProcess.IsFullPaid = true;
                            orderProcess.IsFullPaidDate = DateTime.Now;
                            orderProcess.IsPartial = false;
                        }
                        else
                        {
                            orderProcess.IsPartial = true;
                        }

                        orderProcess.UpdatedTime = DateTime.Now;
                        _dbContext.OrderProcesses.Update(orderProcess);
                        await _dbContext.SaveChangesAsync();
                    }
                    #endregion OrderProcess

                    #region PaymentImage
                    var paymentImageIds = _paymentImageId;

                    foreach (var items in paymentImageIds)
                    {
                        var paymentImageUpdate = await _dbContext.PaymentImages
                            .FirstOrDefaultAsync(x => x.Id == items, cancellationToken);

                        paymentImageUpdate.PaymentId = _paymentId;
                        paymentImageUpdate.UpdatedTime = DateTime.Now;
                        _dbContext.PaymentImages.Update(paymentImageUpdate);
                        await _dbContext.SaveChangesAsync();
                    }
                    #endregion PaymentImage
                }

                transaction.Commit();

                return "Payment created successfully.";
            }
            catch (ValidationException ex)
            {
                transaction.Rollback();
                //foreach (var imageName in _imageName)
                //{
                //    string folderName = "Payments";
                //    string folderPath = Path.Combine(_host.WebRootPath, "Images", folderName, createBy.Name.Replace(" ", string.Empty), imageName);
                //    if (Directory.Exists(folderPath))
                //    {
                //        Directory.Delete(folderPath, true);
                //    }
                //}
                throw new ValidationException(ex.Errors);
            }
            catch (Exception ex)
            {
                transaction.Rollback();
                //foreach (var imageName in _imageName)
                //{
                //    string folderName = "Payments";
                //    string folderPath = Path.Combine(_host.WebRootPath, "Images", folderName, createBy.Name.Replace(" ", string.Empty), imageName);
                //    if (Directory.Exists(folderPath))
                //    {
                //        Directory.Delete(folderPath, true);
                //    }
                //}
                throw new Exception(ex.Message);
            }
        }
    }
}

How to resolve this issue?


Solution

  • To summarize: When having this kind of exception with your DbContext:

    A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.

    Most probably you forgot to await your async methods.