.NET Core与EF Core中数据库优先集成Identity的合理性疑问
首先可以明确说:这种双上下文模式是合理的,尤其适合你这种Database First的场景——既能通过Scaffold-DbContext快速更新业务实体,又能隔离Identity的自定义配置,避免每次脚手架生成时覆盖身份认证相关的代码。不过在后续使用中,确实需要注意几个潜在问题,下面详细说明:
一、双上下文模式的合理性
- 符合关注点分离原则:业务实体(由脚手架生成)和身份认证实体(Identity相关)属于不同的领域,分开管理能让代码更清晰,也方便单独更新某一部分(比如修改数据库表结构后,直接
scaffold -f覆盖业务实体,不会影响Identity的自定义配置)。 - 避免冲突:Identity的默认表结构(如
AspNetUsers、AspNetRoles)和你的业务表是独立的,双上下文各自负责对应的实体,不会出现脚手架生成时误删Identity相关映射的情况。
二、后续关联用户与其他实体可能遇到的问题
1. 事务一致性问题
如果需要同时操作两个上下文的实体(比如创建用户的同时创建关联的业务实体,如Order),默认情况下两个上下文会使用独立的数据库连接,可能出现“部分操作成功、部分失败”的情况,破坏数据一致性。
2. 实体关联的映射持久化问题
当你的业务实体需要关联ApplicationUser时(比如Order属于某个用户),脚手架生成的业务实体不会自动包含与Identity实体的关联——因为AspNetUsers表不在ApplicationContext的管辖范围内。而且直接在脚手架生成的实体类里添加导航属性或外键,下次执行scaffold -f时会被覆盖。
3. 上下文连接的一致性问题
如果两个上下文使用不同的数据库连接实例,可能出现缓存不一致、锁冲突等问题,比如一个上下文读取到的数据是另一个上下文未提交的修改。
三、解决方案与优化建议
1. 处理事务一致性
可以通过共享数据库连接或事务来解决:
- 共享连接:在注册上下文时,确保两个上下文使用同一个连接实例。比如:
services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddDbContext<IdentityContext>(options => options.UseSqlServer(sp => sp.GetRequiredService<ApplicationContext>().Database.GetDbConnection())); - 使用事务:在需要跨上下文操作时,手动开启共享事务:
using var transaction = await _applicationContext.Database.BeginTransactionAsync(); try { // 操作ApplicationContext的实体 await _applicationContext.SaveChangesAsync(); // 把事务关联到IdentityContext await _identityContext.Database.UseTransactionAsync(transaction.GetDbTransaction()); // 操作IdentityContext的实体 await _identityContext.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }
2. 持久化实体关联配置
为了避免脚手架覆盖自定义的关联配置,推荐使用分部类+IEntityTypeConfiguration的组合:
- 第一步:给脚手架生成的业务实体创建分部类,添加关联属性:
// 这个类不会被scaffold覆盖,放在单独的文件中 public partial class Order { public string UserId { get; set; } public ApplicationUser User { get; set; } } - 第二步:创建配置类,定义关联关系:
public class OrderConfiguration : IEntityTypeConfiguration<Order> { public void Configure(EntityTypeBuilder<Order> builder) { builder.HasOne(o => o.User) .WithMany(u => u.Orders) // 根据实际业务关系调整,比如用户有多个订单 .HasForeignKey(o => o.UserId) .HasPrincipalKey(u => u.Id) .OnDelete(DeleteBehavior.Restrict); // 根据需求设置删除行为 } } - 第三步:给
ApplicationContext创建分部类,在OnModelCreating中应用配置:// 这个分部类不会被scaffold覆盖 public partial class ApplicationContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new OrderConfiguration()); // 可以添加更多自定义配置 } }
3. 可选:合并上下文(如果更适合你的场景)
如果觉得双上下文太繁琐,也可以把Identity合并到ApplicationContext中,但要注意:
- 先通过
Scaffold-DbContext生成业务实体。 - 手动在
ApplicationContext中添加Identity的DbSet和配置:
这种方式的缺点是每次public partial class ApplicationContext : IdentityDbContext<ApplicationUser> { public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { } // 脚手架生成的业务DbSet会自动在这里 public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 先执行Identity的配置 // 脚手架生成的业务实体配置会在这里,不要手动修改这部分 // 把自定义配置放到单独的IEntityTypeConfiguration中,避免被scaffold覆盖 modelBuilder.ApplyConfiguration(new OrderConfiguration()); } }scaffold -f时会覆盖OnModelCreating方法,所以必须把自定义配置放到外部的配置类中,不能直接写在OnModelCreating里。
总结
双上下文模式完全适用于你的场景,只要处理好事务一致性和关联配置的持久化问题,后续创建用户与其他实体的关系不会有大问题。这种模式的优势在于能很好地隔离业务代码和Identity代码,同时保留Database First的便利性。
内容的提问来源于stack exchange,提问作者Hazza

