.NET 8 EF Core DDD聚合根模式下保存变更触发并发异常
我正在开发一个人力资源管理项目,使用.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

