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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 22:05:24