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

如何用EF Core与DTO维护一对多关联?移除操作失效排查

问题描述

我正在尝试用DTO和EF Core编辑Session的Trainees学员列表,实现用户的添加与移除功能。目前添加用户能成功持久化,但移除操作无法生效,请问我遗漏了什么?

Domain 代码

public class Session : Entity
{
    #region FIELDS
    private string title;
    private string description;
    private DateTime startsAt;
    private DateTime endsAt;
    private List<User> trainees = new();
    #endregion

    #region PROPERTIES
    public User Coach { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public DateTime? CreatedOn { get; set; }
    public Workout Workout { get; set; }

    public DateTime StartsAt 
    {
        get { return startsAt;  }
        set { startsAt = Guard.Against.Null(value, nameof(startsAt)); } 
    }
    public DateTime EndsAt
    {
        get { return endsAt; }
        set { endsAt = Guard.Against.Null(value, nameof(endsAt)); }
    }

    public string Title 
    {
        get { return title; }
        set { title = Guard.Against.NullOrWhiteSpace(value, nameof(title)); }
    }

    public string Description 
    {
        get { return description; }
        set { description = Guard.Against.NullOrWhiteSpace(value, nameof(description)); }
    }

    public IReadOnlyCollection<User> Trainees => trainees.AsReadOnly();
    #endregion

    #region CONSTRUCTORS
    private Session() { }

    public Session(string title, string description, DateTime startsAt, DateTime endsAt, User coach,Workout workout)
    {
        Title = title;
        Description = description;
        StartsAt = startsAt;
        EndsAt = endsAt;
        Coach = Guard.Against.Null(coach);
        Workout = Guard.Against.Null(workout);
    }
    #endregion

    #region METHODS
    public void AddReservation(User user)
    {
        if (trainees.Count >= 6) throw new ArgumentException("Session is fully booked");
        trainees.Add(user);
        user.AddUserWorkout(Workout);
    }
    public void RemoveReservation(User user)
    {
        trainees.Remove(user);
        user.RemoveUserWorkout(Workout);
    }
    #endregion
}

DTO 代码

public static class SessionDto
{
    public class Index
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public DateTime StartsAt { get; set; }
        public DateTime EndsAt { get; set; }
        public string Description { get; set; }
        public WorkoutDto.Index Workout { get; set; }
        public UserDto.Index Coach { get; set; }
    }

    public class Detail : Index
    {
        public List<UserDto.Index>? Trainees { get; set; }
    }
    
    public class MutateTrainees
    {
        public int TraineeId { get; set; }
        public class Validator : AbstractValidator<Mutate>
        {
            public Validator()
            {

            }
        }
    }
}

Services 代码

public async Task<SessionResponse.MutateTrainee> AddTrainee(SessionRequest.MutateTrainee request)
{
    SessionResponse.MutateTrainee res = new();
    var session = await GetSessionById(request.SessionId).SingleOrDefaultAsync();
    var user = await GetUserById(request.MutateTrainees.TraineeId).SingleOrDefaultAsync();

    Guard.Against.Null(session, nameof(session));
    Guard.Against.Null(user, nameof(user));
    session.AddReservation(user);

    DbCtx.Entry(user).State = EntityState.Modified;
    DbCtx.Entry(session).State = EntityState.Modified;

    await DbCtx.SaveChangesAsync();

    res.SessionId = session.Id;

    return res;
}

public async Task<SessionResponse.MutateTrainee> RemoveTrainee(SessionRequest.MutateTrainee request)
{
    SessionResponse.MutateTrainee res = new();
    var session = await GetSessionById(request.SessionId).SingleOrDefaultAsync();
    var user = await GetUserById(request.MutateTrainees.TraineeId).SingleOrDefaultAsync();

    Guard.Against.Null(session, nameof(session));
    Guard.Against.Null(user, nameof(user));
    session.RemoveReservation(user);

    DbCtx.Entry(session).State = EntityState.Modified;
    DbCtx.Entry(user).State = EntityState.Modified;

    await DbCtx.SaveChangesAsync();

    res.SessionId = session.Id;

    return res;
}

问题分析与解决方案

核心问题

移除操作不生效主要是以下几个原因:

  1. Session未加载Trainees集合:查询Session时没有Include(s => s.Trainees),导致trainees列表是未被EF跟踪的空集合,内存中移除操作无法被EF感知。
  2. 手动设置EntityState.Modified无效:这个操作只会标记实体的标量属性为修改,不会处理集合导航属性的变化。
  3. 实例匹配问题:从数据库获取的User实例和Session.Trainees中的实例可能不是同一个(如果查询时未跟踪),导致trainees.Remove(user)找不到要移除的对象。

具体修复步骤

1. 查询Session时强制加载Trainees集合

修改GetSessionById方法,确保EF跟踪Trainees集合:

private IQueryable<Session> GetSessionById(int sessionId)
{
    return DbCtx.Sessions
        .Include(s => s.Trainees) // 必须加载关联的Trainees
        .Where(s => s.Id == sessionId);
}

2. 确保User实例被上下文跟踪

修改GetUserById,不要使用AsNoTracking(),保证返回的User是EF跟踪的实例:

private IQueryable<User> GetUserById(int userId)
{
    return DbCtx.Users.Where(u => u.Id == userId);
    // 避免添加AsNoTracking()
}

3. 删除手动设置EntityState的代码

EF会自动跟踪跟踪实体的集合变化,不需要手动标记修改,移除以下两行:

// 移除这两行代码
// DbCtx.Entry(session).State = EntityState.Modified;
// DbCtx.Entry(user).State = EntityState.Modified;

4. 优化Remove逻辑(可选但更可靠)

把trainees.Remove(user)改成通过主键匹配移除,避免实例不匹配的问题:

public void RemoveReservation(User user)
{
    trainees.RemoveAll(u => u.Id == user.Id); // 按主键匹配移除
    user.RemoveUserWorkout(Workout);
}

5. 检查User的RemoveUserWorkout逻辑

确保user.RemoveUserWorkout(Workout)也正确处理了关联关系,查询User时也要Include对应的UserWorkouts集合,让EF跟踪这个变化。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 21:30:32