【问题标题】:Error in EF Core: A second operation started on this context before a previous operation completedEF Core 中的错误:在前一个操作完成之前在此上下文上启动了第二个操作
【发布时间】:2019-05-25 16:19:26
【问题描述】:

我的 web api 应用程序出现奇怪的错误: 在前一个操作完成之前,在此上下文上开始了第二个操作。不保证任何实例成员都是线程安全的。

我有一个流程来计算用户的发薪日并更新其用户休假余额。所以我需要迭代用户并获得其休假余额,然后对每个余额进行更新。我还不知道如何解决这个错误。 当我调用 this.SaveAll() 时触发的错误是包含 b

    public async Task<bool> SaveAll()
    {
        return await _context.SaveChangesAsync() > 0;
    }

datacontext 也被注入此代码:

private readonly DataContext _context;
    private readonly IAdminSettingsRepository _settingRepo;
    private readonly IAppRepository _appRepository;
    public PayrollRepository(DataContext context, IAdminSettingsRepository settingRepo, IAppRepository appRepository)
    {
        _context = context;
        _settingRepo = settingRepo;
        _appRepository = appRepository;
    }

流程如下:

public async Task<bool> ProcessPayCalendar(PayCalendar payCalendar)
        {
            List<User> users = payCalendar.Users.ToList();
            SickLeaveEntitlement sickLeaveEntitlement = await _settingRepo.GetSickLeaveEntitlement();
            AnnualLeaveEntitlement annualLeaveEntitlement = await _settingRepo.GetAnnualLeaveEntitlement();
            LongServiceLeaveEntitlement longServiceLeaveEntitlement = await _settingRepo.GetLongServiceLeaveEntitlement();
            DateTime tenYearsAgo = DateTime.Today.AddYears(-10);
            DateTime currentMonth = new DateTime(payCalendar.NextPaymentDate.Year, payCalendar.NextPaymentDate.Month, 1);
            DateTime previousMonth = currentMonth.AddMonths(-1);


            /// begin calculation for leave calendar
            users.ForEach(async user =>
            {
                decimal hoursWorked = this.CalculateWorkingHours(user);
                decimal hourlyRate = this.CalculateHourlyRate(user);
                // create pay day for user
                Payday payday = new Payday();
                payday.UserId = user.Id;
                payday.PayPeriodStart = payCalendar.PayPeriodStartDate;
                payday.PayPeriodEnd = payCalendar.PayPeriodEndDate;
                payday.PaymentDate = payCalendar.NextPaymentDate;
                payday.HoursWorked = hoursWorked;
                payday.SickLeaveAccrual = this.CalculateSickLeaveEntitlement(hoursWorked, sickLeaveEntitlement);
                payday.AnnualLeaveAccrual = this.CalculateAnnualLeaveEntitlement(hoursWorked, annualLeaveEntitlement);
                payday.LongServiceLeaveAccrual = (user.StartDateCurrentAnnualSalary > tenYearsAgo) ? 0m : this.CalculateLongServiceEntitlement(hoursWorked, longServiceLeaveEntitlement);
                payday.SickLeaveAccrualValue = payday.SickLeaveAccrual * hourlyRate;
                payday.AnnualLeaveAccrualValue = payday.AnnualLeaveAccrual * hourlyRate;
                payday.LongServiceLeaveAccrualValue = payday.LongServiceLeaveAccrual * hourlyRate;

                this.Add(payday);

                // do calculation on leave balance 
                // jika leave balance di awal bulan maka lakukan replikasi dr bulan sebelumnya.
                // closebalance menjadi opening balance bulan selanjutnya.
                LeaveBalance sickLeaveBalance = await this.GetUserLeaveBalance(user.Id, sickLeaveEntitlement, "sickLeave");
                if (sickLeaveBalance == null)
                {
                    // sickLeaveBalance =  await this.CreateLeaveBalance(user.Id,sickLeaveEntitlement,"sickLeave",payday.PaymentDate);
                    // sickLeaveBalance.CurrentBalance =+ payday.SickLeaveAccrual ;
                    // sickLeaveBalance.CurrentBalanceValue += payday.SickLeaveAccrualValue;
                    throw new Exception($"Sick Leave balance for user: {user.Username} is not found. Please report this as bug");
                }
                else
                {
                    sickLeaveBalance.CurrentBalance = +payday.SickLeaveAccrual;
                    sickLeaveBalance.CurrentBalanceValue += payday.SickLeaveAccrualValue;
                    sickLeaveBalance.LastUpdate = payday.PaymentDate;
                }

                LeaveBalance annualLeaveBalance = await this.GetUserLeaveBalance(user.Id, annualLeaveEntitlement, "annualLeave");
                if (annualLeaveBalance == null)
                {
                    throw new Exception($"Annual Leave balance for user: {user.Username} is not found. Please report this as bug");
                }
                else
                {
                    annualLeaveBalance.CurrentBalance = +payday.AnnualLeaveAccrual;
                    annualLeaveBalance.CurrentBalanceValue += payday.AnnualLeaveAccrualValue;   
                    annualLeaveBalance.LastUpdate = payday.PaymentDate;

                }
                LeaveBalance longServiceLeaveBalance = await this.GetUserLeaveBalance(user.Id, longServiceLeaveEntitlement, "longServiceLeave");
                if (longServiceLeaveBalance == null)
                {
                    throw new Exception($"Long Service Leave balance for user: {user.Username} is not found. Please report this as bug");
                }
                else
                {
                    longServiceLeaveBalance.CurrentBalance = +payday.LongServiceLeaveAccrual;
                    longServiceLeaveBalance.CurrentBalanceValue += payday.LongServiceLeaveAccrualValue;
                    longServiceLeaveBalance.LastUpdate = payday.PaymentDate;

                }
            });
            return await this.SaveAll();
        }

完整的错误:

失败:Microsoft.EntityFrameworkCore.Update[10000] 保存上下文类型“CRSApp.API.Data.DataContext”的更改时,数据库中发生异常。 System.InvalidOperationException:在前一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。 在 Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection() 在 Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IReadOnlyList1 entriesToSave, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) System.InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe. at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection() at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IReadOnlyList1 entriesToSave,CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(布尔型 acceptAllChangesOnSuccess,CancellationToken cancelToken) 在 Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(布尔型 acceptAllChangesOnSuccess,CancellationToken cancelToken) 失败:Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1] 执行请求时发生未处理的异常。 System.InvalidOperationException:在前一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。 在 Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection() 在 Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IReadOnlyList1 entriesToSave, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) at CRSApp.API.Data.PayrollRepository.SaveAll() in E:\CRSApp\crsapp.api\Data\PayrollRepository.cs:line 35 at CRSApp.API.Data.PayrollRepository.ProcessPayCalendar(PayCalendar payCalendar) in E:\CRSApp\crsapp.api\Data\PayrollRepository.cs:line 229 at CRSApp.API.Controllers.Admin.PayrollController.ProcessPayCalendar(PayCalendarParam param) in E:\CRSApp\crsapp.api\Controllers\Admin\PayrollController.cs:line 68 at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at System.Threading.Tasks.ValueTask1.get_Result() 在 Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync() 在 Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync() 在 Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext 上下文) 在 Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(状态&下一个,范围&范围,对象&状态,布尔& isCompleted) 在 Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() 在 Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() 在 Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext 上下文) 在 Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(状态&下一个,范围&范围,对象&状态,布尔& isCompleted) 在 Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() 在 Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync() 在 Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) 在 Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext 上下文) 在 Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware.Invoke(HttpContext 上下文) 在 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext 上下文) 在 Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext 上下文)

仅供参考:我使用 Dotnet Core 2.1.1,如下所述。

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.1"/>
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.0" PrivateAssets="All"/>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="4.0.1"/>
    <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.1"/>
    <PackageReference Include="MailKit" Version="2.0.6"/>
  </ItemGroup>

【问题讨论】:

  • 您如何将上下文和存储库添加到您的 DI 容器中?您是否将它们添加为单例?
  • services.AddScoped(); services.AddDbContext( opt => opt.UseMySql( Configuration.GetConnectionString("DefaultConnection") ) );
  • 我尝试了 ServiceLifetime.Transient。在 Dbcontext 但它会产生另一个错误。

标签: asp.net .net asp.net-core entity-framework-core


【解决方案1】:

DbContext 不是线程安全的

我认为ForEach 是你的问题。

 users.ForEach(async user =>
        {
           //....

            LeaveBalance sickLeaveBalance = await this.GetUserLeaveBalance(user.Id,});
           // .....
        });

由于您使用了async 关键字,因此对于每个用户,都会异步调用一个操作,因此您的代码执行将类似于以下代码的执行:

foreach(var user in users){
     //without await
     //it's an async method
    DoSomeThingAsync(user); //you called GetUserLeaveBalance in DoSomeThingAsync
}

我猜,GetUserLeaveBalance 使用的是DbContext,所以你使用的是DbContext 异步,你会遇到这样的错误

在前一个操作之前在此上下文上启动了第二个操作 完成...

您需要将您的 ForEach 更改为类似的内容

foreach(var user in users){
    await DoSomeThingAsync(user); //you called GetUserLeaveBalance in DoSomeThingAsync
}

【讨论】:

  • 但你不能在没有异步用户的情况下在循环内执行异步 DoSomethingAsync()。
  • 因为异步会导致问题,我现在在同步模式下创建它。它工作。
  • 我通过 var tasks = new List>(); 修复代码在循环中我放了 tasks.Add(DoSomethingAsync(user));在循环之外我运行 Task.WhenAll(tasks);现在它工作了
  • 你在Task.WhenAll(tasks)之前用过await吗?因为据我所知,如果多个线程同时尝试在 DbContext 的实例上使用它会导致异常。
猜你喜欢
  • 2020-11-06
  • 2021-05-04
  • 2018-11-07
  • 1970-01-01
  • 1970-01-01
  • 2020-06-05
  • 2019-12-17
  • 2018-07-23
  • 1970-01-01
相关资源
最近更新 更多