You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无并发请求时EF Core抛出DbUpdateConcurrencyException的原因排查

EF Core更新考试尝试时触发DbUpdateConcurrencyException问题

我正在开发一个用Entity Framework Core管理考试尝试与答案的ASP.NET Core Web API。在Update方法中,我获取ExamAttempt实例,修改其关联的答案,再通过UnitOfWork.Save()保存更改,但一直碰到DbUpdateConcurrencyException异常,提示信息如下:

The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.

我是唯一测试该接口的人,理论上不该存在并发请求或数据变更。

相关代码片段

public async Task<OperationResult<ExamAttemptReponseDto>> Update(Guid id, UpdateExamAttemptDto attemptDto, Guid studentId)
{
    var includeProperties = "ExamAttemptAnswers,Exam";
    var ExamAttempt = await UnitOfWork.ExamAttempt.Get(a => a.Id == id, includeProperties);

    if (ExamAttempt == null) 
    {
        return OperationResult<ExamAttemptReponseDto>.Failure("Exam attempt not found");
    }

    // 获取所有考试题目
    var examQuestions = await UnitOfWork.ExamQuestion
                                        .GetAll("Question, Question.QuestionChoices", q => q.ExamId == ExamAttempt.ExamId);

    // 计算考试结束时间
    var ExamEndDate = ExamAttempt.Exam.StartAt.AddMinutes(ExamAttempt.Exam.DurationInMinutes);

    var canModify = ((ExamAttempt.StudentId == studentId) && 
                     (ExamEndDate >= DateTime.Now) && 
                     ExamAttempt.SubmittedAt == null);

    if (!canModify) 
    {
        return OperationResult<ExamAttemptReponseDto>.Failure("You no longer can modify this attempt");
    }

    var answersToAdd = new List<ExamAttemptAnswer>();

    foreach (var updatedAnswer in attemptDto.answers)
    {
        var associatedQuestion = examQuestions.ToList().Find(q => q.QuestionId == updatedAnswer.QuestionId);

        if (associatedQuestion == null) 
        {
            return OperationResult<ExamAttemptReponseDto>.Failure("The question you're trying to access is not found in this exam");
        }

        var questionHasAnswers = ExamAttempt.ExamAttemptAnswers.Any(a => a.QuestionId == associatedQuestion.Id);

        if (questionHasAnswers && associatedQuestion.Question is MultiChoiceQuestion) 
        {
            return OperationResult<ExamAttemptReponseDto>.Failure("You can't add more answers to MCQ");
        }

        var questionChoiceBelongsToQuestion = associatedQuestion.Question.QuestionChoices
            .Any(c => c.Id == updatedAnswer.AnswerId);

        if (!questionChoiceBelongsToQuestion)
        {
            return OperationResult<ExamAttemptReponseDto>.Failure("The answer you're trying to select doesn't belong to the question");
        }

        var answerAlreadyExists = ExamAttempt.ExamAttemptAnswers
            .Any(a => a.QuestionId == associatedQuestion.Id && a.AnswerId == updatedAnswer.AnswerId);

        if (answerAlreadyExists) 
        {
            continue;
        }

        answersToAdd.Add(new ExamAttemptAnswer() {
            Id = Guid.NewGuid(),
            ExamAttemptId = ExamAttempt.Id,
            AnswerId = updatedAnswer.AnswerId,
            LastModified = DateTime.Now,
            QuestionId = associatedQuestion.Id
        });
    }

    ExamAttempt.ExamAttemptAnswers.AddRange(answersToAdd);
    UnitOfWork.ExamAttempt.Update(ExamAttempt);

    var result = await UnitOfWork.Save();

    if (result < attemptDto.answers.Count) 
    {
        return OperationResult<ExamAttemptReponseDto>.Failure("Can't update attempt answers");
    }

    return OperationResult<ExamAttemptReponseDto>.Success(null);
}

已尝试的排查步骤

  • 确认无其他进程修改数据
  • 检查已正确加载对应的ExamAttempt及关联实体
  • 验证无其他用户并发更新数据

可能的原因及解决办法

1. 多余的Update调用导致状态异常

你在添加完新的ExamAttemptAnswer后调用了UnitOfWork.ExamAttempt.Update(ExamAttempt),但ExamAttempt是从数据库加载的跟踪实体,EF Core会自动跟踪它的所有变更(包括关联集合的新增)。手动调用Update会强制将实体状态设为Modified,EF Core会尝试更新ExamAttempt主表的所有字段,如果此时主实体的并发校验字段(比如RowVersion)没有被正确加载或匹配,就会触发并发异常。

解决办法:删除UnitOfWork.ExamAttempt.Update(ExamAttempt);这一行,直接调用Save()即可。跟踪状态下的实体变更会被EF Core自动检测,无需手动调用Update。

2. 并发令牌配置或加载问题

如果ExamAttempt实体配置了并发令牌(比如带[Timestamp]或[ConcurrencyCheck]标记的字段),但加载实体时没有包含该字段,EF Core会认为数据已被修改。

解决办法:

  • 检查ExamAttempt实体的配置,确保并发令牌字段被包含在includeProperties中(比如如果字段名为RowVersion,则修改为"ExamAttemptAnswers,Exam,RowVersion")。
  • 确认该字段在数据库中是正确生成的(比如SQL Server的ROWVERSION类型)。

3. UnitOfWork的Save方法实现异常

检查UnitOfWork.Save()的代码,确认它只是正确调用DbContext.SaveChangesAsync(),没有额外修改实体状态或过滤变更的逻辑。如果Save方法内部有错误处理或状态重置,可能导致变更未被正确提交。

解决办法:

  • 确保Save方法的核心逻辑是return await _dbContext.SaveChangesAsync();,没有多余的操作。

4. ExamAttemptAnswer的外键或主键问题

虽然你用了Guid.NewGuid()生成ExamAttemptAnswer的主键,但如果数据库中该字段的主键策略不是手动赋值,或者ExamAttemptId与当前ExamAttempt.Id不匹配,可能导致新增失败,进而引发主实体的更新检测异常。

解决办法:

  • 确认ExamAttemptAnswer的主键配置允许手动赋值(比如Fluent API中使用HasKey(x => x.Id).ValueGeneratedNever())。
  • 调试时检查ExamAttempt.Id和新增ExamAttemptAnswer的ExamAttemptId是否完全一致。

内容的提问来源于stack exchange,提问作者Marko Sami

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 20:15:59