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

.NET 8 EF Core DDD聚合根模式下保存变更触发并发异常

问题:EF Core 保存聚合根下属实体时出现并发异常

我正在开发一个人力资源管理项目,使用.NET 8和EF Core实现实体变更保存。项目采用DDD聚合根设计模式,Employee为聚合根,LeaveRequest是其下属实体。执行命令处理器保存变更时,抛出DbUpdateConcurrencyException,提示预期影响1行但实际0行。

相关代码

控制器代码

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class LeaveController : BaseController
{
    private readonly IMediator _mediator;

    public LeaveController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> SubmitLeaveRequest([FromBody] SubmitLeaveRequest submitLeaveRequest)
    {
        var command = new SubmitLeaveRequestCommand(GetCurrentUserId(), submitLeaveRequest.StartDate, submitLeaveRequest.EndDate, submitLeaveRequest.Reason);

        var data = await _mediator.Send(command);

        return CreateResponse(data);
    }
}

CQRS命令处理器代码

public class SubmitLeaveRequestCommandHandler : IRequestHandler<SubmitLeaveRequestCommand, Unit>
{
    private readonly IEmployeeRepository _repository;

    public SubmitLeaveRequestCommandHandler(IEmployeeRepository repository)
    {
        _repository = repository;
    }

    public async Task<Unit> Handle(SubmitLeaveRequestCommand request, CancellationToken cancellationToken)
    {
        var employee = await _repository.GetEmployeeByUserIdAsync(request.UserId);

        if (employee == null)
            throw new Exception("Not Found");

        employee.AddLeaveRequest(request.StartDate, request.EndDate, request.Reason);

        await _repository.SaveChanges(cancellationToken);

        return Unit.Value;
    }
}

仓储SaveChanges方法

public async Task SaveChanges(CancellationToken cancellationToken)
{
    await _context.SaveChangesAsync(cancellationToken);
}

实体定义

public class Employee : AggregateRoot
{
    public string UserId { get; private set; }
    public string FirstName { get; set; }
    public string LastName { get; private set; }
    public string Position { get; private set; }
    public Address Address { get; private set; }

    private readonly List<LeaveRequest> _leaveRequests = new List<LeaveRequest>();
    public IReadOnlyCollection<LeaveRequest> LeaveRequests => _leaveRequests.AsReadOnly();

    public Employee() {}

    public Employee(string userId, string firstName, string lastName, string position, Address address)
    {
        Id = Guid.NewGuid();
        UserId = userId;
        FirstName = firstName;
        LastName = lastName;
        Position = position;
        Address = address;

        AddDomainEvent(new EmployeeCreatedEvent(this));
    }

    public void AddLeaveRequest(DateTime startDate, DateTime endDate, string reason)
    {
        var leaveRequest = new LeaveRequest(startDate, endDate, reason, Id);
        _leaveRequests.Add(leaveRequest);
        AddDomainEvent(new LeaveRequestAddedEvent(leaveRequest));
    }
}

public class LeaveRequest : Entity
{
    public DateTime StartDate { get; private set; }
    public DateTime EndDate { get; private set; }
    public string Reason { get; private set; }
    public bool IsApproved { get; private set; }
    public string? RejectionReason { get; private set; }
    public Guid EmployeeId { get; set; }

    public LeaveRequest() {}

    public LeaveRequest(DateTime startDate, DateTime endDate, string reason, Guid employeeId)
    {
        Id = Guid.NewGuid();
        StartDate = startDate;
        EndDate = endDate;
        Reason = reason;
        IsApproved = false;
        EmployeeId = employeeId;
    }

    public void Approve()
    {
        IsApproved = true;
    }

    public void Reject(string rejectionReason)
    {
        IsApproved = false;
        RejectionReason = rejectionReason;
    }
}

上下文配置

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Employee> Employees { get; set; }
    public DbSet<LeaveRequest> LeaveRequests { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Ignore<DomainEvent>();

        modelBuilder.Entity<Employee>()
        .OwnsOne(e => e.Address, a =>
        {
            a.Property(ad => ad.Street).HasColumnName("Street").IsRequired();
            a.Property(ad => ad.City).HasColumnName("City").IsRequired();
            a.Property(ad => ad.State).HasColumnName("State").IsRequired();
            a.Property(ad => ad.ZipCode).HasColumnName("ZipCode").IsRequired();
        });

        modelBuilder.Entity<Employee>()
           .HasMany(e => e.LeaveRequests);

        modelBuilder.Entity<Employee>()
            .HasIndex(e => e.UserId)
            .IsUnique();

        base.OnModelCreating(modelBuilder);
    }
}

EF Core服务配置

services.AddDbContext<AppDbContext>
(
    options =>
    {
        options.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll);
        options.UseSqlServer
        (
            configuartion.GetConnectionString("DefaultConnection")
        );
    }
);

错误信息

Microsoft.EntityFrameworkCore.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.


解决方法

1. 补全聚合根的并发令牌配置

AggregateRoot基类大概率缺少乐观并发控制的令牌配置,这是触发该异常的常见原因:

  • 在AggregateRoot中添加并发令牌属性:
    public byte[] RowVersion { get; private set; } = Array.Empty<byte>();
    
  • 在上下文配置中标记该属性为行版本:
    modelBuilder.Entity<Employee>()
        .Property(e => e.RowVersion)
        .IsRowVersion();
    
  • 确保查询Employee时加载该属性,避免投影导致令牌缺失。

2. 修复一对多关系映射

当前的HasMany配置不完整,EF Core无法明确关联规则,可能错误地尝试更新Employee实体而非仅插入LeaveRequest:

  • 显式配置外键和关联规则:
    modelBuilder.Entity<Employee>()
        .HasMany(e => e.LeaveRequests)
        .WithOne()
        .HasForeignKey(lr => lr.EmployeeId)
        .OnDelete(DeleteBehavior.Cascade);
    

3. 验证仓储查询的跟踪状态

虽然全局开启了TrackAll,但需确保GetEmployeeByUserIdAsync方法没有单独设置AsNoTracking(),否则EF Core无法跟踪实体变更,保存时会触发异常。

4. 确认Employee实体的有效性

检查查询返回的Employee是否真的存在于数据库中:

  • 打印employee.Id并核对数据库记录,排除因查询条件错误(如UserId大小写不匹配)导致的无效实体加载。

5. 排查并发修改情况

若在获取Employee和保存操作之间,有其他进程/请求修改或删除了该记录,也会触发异常。可添加日志记录操作前后的Employee信息和时间戳,定位并发来源。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 19:47:05