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

EF Core中保存DDD聚合根User时Upsert子实体Appointment的实现异常排查

解决EF Core聚合根子实体插入时的乐观并发异常问题

你遇到的乐观并发异常,根源在于直接替换聚合根的子实体集合导致EF Core的变更跟踪误判,尝试删除一个从未被持久化到数据库的子实体。

错误原因详解

测试与仓库代码的交互过程中出现了以下冲突:

  1. 测试中,你从数据库加载了被上下文跟踪的ApplicationUserEntity,并给它的Appointments集合添加了一个新的AppointmentEntity(此时这个子实体仅存在于内存,状态为Added)。
  2. 在仓库的Update方法中,你直接将persistenceUserEntity.Appointments赋值为新的List<AppointmentEntity>,EF Core会认为原来集合中的所有子实体(包括那个未保存的新实体)都被移除了。
  3. 调用Save时,EF Core尝试执行DELETE语句删除那个从未被插入数据库的子实体,自然影响0行,触发乐观并发异常。

修复方案:操作集合内容而非替换整个集合

不要直接替换Appointments集合,而是通过更新现有子实体、添加新子实体、移除不再存在的子实体的方式维护集合,让EF Core的变更跟踪能正确识别每个子实体的状态:

public async Task Update(IApplicationUserWithAppointments domainUserEntity)
{
    ApplicationUserEntity persistenceUserEntity = await FindEntityById(domainUserEntity.Id);
    var existingAppointmentsById = persistenceUserEntity.Appointments
        .ToDictionary(appt => appt.Id);
    var domainAppointmentsById = domainUserEntity.Appointments
        .ToDictionary(appt => appt.Id);

    // 更新用户根实体属性
    persistenceUserEntity.UserName = domainUserEntity.UserName;
    persistenceUserEntity.Password = domainUserEntity.Password;
    persistenceUserEntity.FirstName = domainUserEntity.FirstName;
    persistenceUserEntity.LastName = domainUserEntity.LastName;
    persistenceUserEntity.Role = domainUserEntity.Role;
    persistenceUserEntity.Validated = domainUserEntity.Validated;

    // 处理子实体:更新已有,添加新的
    foreach (var domainAppointment in domainUserEntity.Appointments)
    {
        if (existingAppointmentsById.TryGetValue(domainAppointment.Id, out var existingAppt))
        {
            // 更新已有预约的属性
            existingAppt.State = domainAppointment.State.Name;
            existingAppt.DateTime = domainAppointment.DateTime;
        }
        else
        {
            // 添加新预约到集合
            persistenceUserEntity.Appointments.Add(new AppointmentEntity(domainAppointment));
        }
    }

    // 移除领域对象中不再存在的预约(如果业务允许删除)
    foreach (var existingAppt in existingAppointmentsById.Values.ToList())
    {
        if (!domainAppointmentsById.ContainsKey(existingAppt.Id))
        {
            persistenceUserEntity.Appointments.Remove(existingAppt);
        }
    }

    // 无需调用Update:persistenceUserEntity已被上下文跟踪,变更会自动被检测
    // this.context.Users.Update(persistenceUserEntity);
}

额外优化建议

  1. 移除不必要的Update调用:如果FindEntityById是通过EF Core的跟踪查询(比如FindAsync或带跟踪的FirstAsync)获取的实体,上下文已经在跟踪它的变更,不需要手动调用context.Users.Update,否则会强制将整个实体标记为Modified,生成不必要的UPDATE语句。
  2. 遵循DDD规范修改测试代码:测试中直接修改持久化类ApplicationUserEntity不符合DDD实践,应该先操作领域对象再传给仓库更新:
// 改进后的测试Given阶段
var domainUser = await this.dbDataFactory.InsertValidatedLifeAssistant()
    .ToDomainEntity(new AppointmentStateFactory());
domainUser.Appointments.Add(new Appointment(
    Guid.NewGuid(),
    AppointmentState.Planned,
    DateTime.Today.AddDays(3)));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 18:03:11