ASP.NET EF Core多对多建组时重复插入用户报错求解决
使用JS客户端搭配ASP.NET EF Core后端,客户端向后端发送包含用户对象的组对象,需求是创建组并关联已存在的用户。但执行数据库插入操作时,EF Core尝试向AspNetUsers表重复插入用户,触发主键约束冲突报错:
SqlException: The PK_AspNetUsers PRIMARY KEY constraint was violated. Failed to insert duplicate key into object 'dbo.AspNetUsers'. Duplicate switch value:
EF Core生成的SQL语句会同时插入AspNetUsers、Groups和UserGroups三张表。
已知从后端直接查询用户并设置EntityState.Unchanged状态后,可正常执行(仅插入Groups和UserGroups表),但不想重新从数据库查询用户,希望直接将组中用户导航属性设为Unchanged状态,求实现方法。
相关实体与配置
User类
public partial class User : IdentityUser { public ICollection<Group> JoinedGroups { get; set; } // 关联UserGroups表的导航属性 public ICollection<Group> CreatedGroups { get; set; } // 关联Groups表的导航属性 public ICollection<Event> Events { get; set; } = new List<Event>(); public ICollection<Comment> Comments { get; set; } }
Group类
public partial class Group { [Key] public Guid Id { get; set; } public string Name { get; set; } public string UserId { get; set; } [Required] public User? User { get; set; } public List<User> Users { get; set; } = new(); public ICollection<Event> Events { get; } = new List<Event>(); }
DbContext配置
modelBuilder.Entity<Group>() .HasMany(e => e.Users) .WithMany(e => e.JoinedGroups) .UsingEntity( "UserGroups", l => l.HasOne(typeof(User)).WithMany().HasForeignKey("UsersId").HasPrincipalKey(nameof(User.Id)), r => r.HasOne(typeof(Group)).WithMany().HasForeignKey("GroupsId").HasPrincipalKey(nameof(Group.Id)), j => j.HasKey("UsersId", "GroupsId")); modelBuilder.Entity<User>() .HasMany(e => e.Events) .WithOne(e => e.User) .HasForeignKey(e => e.UserId) .IsRequired(); modelBuilder.Entity<User>() .HasMany(e => e.CreatedGroups) .WithOne(e => e.User) .HasForeignKey(e => e.UserId) .OnDelete(deleteBehavior: DeleteBehavior.NoAction) .IsRequired(); modelBuilder.Entity<Group>() .HasMany(e => e.Events) .WithOne(e => e.Group) .HasForeignKey(e => e.GroupIdentificator);
报错的插入代码
public async Task<ServiceResponse> CreateGroupAsync(GroupDTO groupModel, string authUserId) { if (authUserId == null) return ServiceResponse.Fail("You are not authenticated!"); try { groupModel.Id = Guid.NewGuid(); var obj = _mapper.Map<Group>(groupModel); _webDiaryContext.Groups.Add(obj); await _webDiaryContext.SaveChangesAsync(); // 报错位置 return ServiceResponse.Success("Group successfully created!"); } catch (Exception ex) { return ServiceResponse.Fail(ex.Message); } }
可行但不希望使用的代码
public async Task<ServiceResponse> CreateGroupAsync(GroupDTO groupModel, string authUserId) { if (authUserId == null) return ServiceResponse.Fail("You are not authenticated!"); var test = _webDiaryContext.Users.FirstOrDefault(sc => sc.UserName == "test3@mail.ru"); _webDiaryContext.Entry(test).State = EntityState.Unchanged; try { groupModel.Id = Guid.NewGuid(); var obj = _mapper.Map<Group>(groupModel); obj.Users.Add(test); _webDiaryContext.Groups.Add(obj); await _webDiaryContext.SaveChangesAsync(); return ServiceResponse.Success("Group successfully created!"); } catch (Exception ex) { return ServiceResponse.Fail(ex.Message); } }
核心思路是告诉EF Core:这些User实体是数据库中已存在的,不需要插入,只需要建立关联。无需查询数据库,直接构造User实体(仅需主键ID),然后将其状态设为Unchanged或使用Attach方法。
方法1:直接设置用户实体状态为Unchanged
映射得到Group对象后,遍历其Users集合,为每个User设置状态:
public async Task<ServiceResponse> CreateGroupAsync(GroupDTO groupModel, string authUserId) { if (authUserId == null) return ServiceResponse.Fail("未认证!"); try { groupModel.Id = Guid.NewGuid(); var group = _mapper.Map<Group>(groupModel); // 标记关联用户为已存在状态 foreach (var user in group.Users) { // 确保User的主键Id已从DTO映射过来 _webDiaryContext.Entry(user).State = EntityState.Unchanged; } // 处理组的创建者User(如果DTO中包含) if (group.User != null) { _webDiaryContext.Entry(group.User).State = EntityState.Unchanged; } _webDiaryContext.Groups.Add(group); await _webDiaryContext.SaveChangesAsync(); return ServiceResponse.Success("组创建成功!"); } catch (Exception ex) { return ServiceResponse.Fail(ex.Message); } }
方法2:使用Attach方法附加用户实体
Attach方法会自动将主键存在的实体标记为Unchanged状态,效果和手动设置一致:
public async Task<ServiceResponse> CreateGroupAsync(GroupDTO groupModel, string authUserId) { if (authUserId == null) return ServiceResponse.Fail("未认证!"); try { groupModel.Id = Guid.NewGuid(); var group = _mapper.Map<Group>(groupModel); foreach (var user in group.Users) { _webDiaryContext.Users.Attach(user); } if (group.User != null) { _webDiaryContext.Users.Attach(group.User); } _webDiaryContext.Groups.Add(group); await _webDiaryContext.SaveChangesAsync(); return ServiceResponse.Success("组创建成功!"); } catch (Exception ex) { return ServiceResponse.Fail(ex.Message); } }
方法3:优化AutoMapper配置(可选)
如果DTO中的User对象仅包含主键和必要标识字段,可配置AutoMapper只映射主键,减少多余属性,避免EF Core误判为新实体:
// 在AutoMapper配置文件中 CreateMap<UserDTO, User>() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) // 忽略其他不需要映射的属性 .ForAllOtherMembers(opt => opt.Ignore());
内容的提问来源于stack exchange,提问作者kaseta

