同时使用EF Core DbContext与ASP.NET Core Identity UserManager的问题
问题描述
我的用户实体定义如下:
public class User : IdentityUser<long> { public Office Office { get; set; } // 不可空 // ...其他自定义属性 }
尝试创建新用户时执行以下代码:
var user = new User { UserName = "foo", Email = "foo@example.com", EmailConfirmed = true, Office = await context.Offices.SingleAsync(x => x.Name == "Head Office"), }; await _userManager.CreateAsync(user, "password");
抛出错误:
duplicate key value violates unique constraint "PK_Office"
原因是Office实体在一个DbContext中加载,却被UserManager封装的另一个DbContext使用,导致后者尝试创建相同主键的新实体。由于UserManager未暴露底层上下文,无法执行userManager.Context.Attach(office)这类操作。
我不想采用以下两种规避方案:
- 将
User.Office声明为可空类型,先创建用户再关联办公室,但这会破坏领域模型(用户必须关联办公室) - 直接通过DbContext创建用户而非
UserManager,但不想自行实现UserManager处理的认证相关逻辑
请问还有其他可行方案吗?(注:此场景为控制台应用,非Web请求环境)
可行解决方案
方案1:确保UserManager与查询Office使用同一个DbContext实例
问题根源是两个独立的DbContext实例导致实体跟踪冲突,只需让UserManager使用你查询Office时的同一个DbContext即可解决。
实现步骤:
- 确保你的DbContext继承
IdentityDbContext,包含Identity所需的表:
public class AppDbContext : IdentityDbContext<User, IdentityRole<long>, long> { public DbSet<Office> Offices { get; set; } // ...其他DbSet public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } }
- 注册服务时,指定Identity使用你的DbContext:
// 注册DbContext services.AddDbContext<AppDbContext>(options => options.UseSqlServer("你的数据库连接字符串")); // 注册Identity并关联你的DbContext services.AddIdentity<User, IdentityRole<long>>() .AddEntityFrameworkStores<AppDbContext>() .AddDefaultTokenProviders();
- 使用时,注入同一个
AppDbContext实例查询Office,再通过UserManager创建用户:
// 注入AppDbContext和UserManager<User> private readonly AppDbContext _context; private readonly UserManager<User> _userManager; public YourService(AppDbContext context, UserManager<User> userManager) { _context = context; _userManager = userManager; } public async Task CreateUser() { var office = await _context.Offices.SingleAsync(x => x.Name == "Head Office"); var user = new User { UserName = "foo", Email = "foo@example.com", EmailConfirmed = true, Office = office }; await _userManager.CreateAsync(user, "password"); }
此时Office实体已被当前DbContext跟踪,UserManager保存用户时只会建立关联,不会重复插入Office。
方案2:仅关联Office主键(使用实体Stub)
如果无法共享DbContext,可以只创建包含Office主键的Stub实体,避免加载完整实体,同时在你的DbContext中将其标记为已存在。
代码示例:
// 只查询Office的主键,不加载完整实体 var officeId = await _context.Offices .Where(x => x.Name == "Head Office") .Select(x => x.Id) .SingleAsync(); // 创建User并关联仅含主键的Office Stub var user = new User { UserName = "foo", Email = "foo@example.com", EmailConfirmed = true, Office = new Office { Id = officeId } }; // 将Office Stub标记为已存在(Unchanged),避免被视为新实体 _context.Attach(user.Office).State = EntityState.Unchanged; // 创建用户 await _userManager.CreateAsync(user, "password");
这种方式不需要加载完整的Office实体,同时告诉DbContext该Office已存在,UserManager处理时不会尝试插入重复数据。
方案3:自定义UserManager,添加实体Attach方法
如果需要保留原有DbContext分离的场景,可以自定义UserManager,暴露Attach实体的方法,让你能手动将Office实体标记为已存在。
实现步骤:
- 创建自定义
UserManager:
public class CustomUserManager : UserManager<User> { private readonly AppDbContext _context; public CustomUserManager( IUserStore<User> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<User> passwordHasher, IEnumerable<IUserValidator<User>> userValidators, IEnumerable<IPasswordValidator<User>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<User>> logger, AppDbContext context) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { _context = context; } // 添加Attach实体的方法 public void AttachExistingEntity<TEntity>(TEntity entity) where TEntity : class { if (_context.Entry(entity).State == EntityState.Detached) { _context.Attach(entity).State = EntityState.Unchanged; } } }
- 注册自定义
UserManager:
services.AddScoped<UserManager<User>, CustomUserManager>();
- 使用自定义
UserManager创建用户:
var office = await _context.Offices.SingleAsync(x => x.Name == "Head Office"); var user = new User { UserName = "foo", Email = "foo@example.com", EmailConfirmed = true, Office = office }; // 先Attach Office实体,标记为已存在 _customUserManager.AttachExistingEntity(office); // 创建用户 await _customUserManager.CreateAsync(user, "password");
内容的提问来源于stack exchange,提问作者lonix
相关产品推荐
相关产品推荐

