EF Core多DbContext关联与迁移冲突问题求助
问题
我想把通知功能拆分到独立类库项目,使用单独的NotificationDbContext。单应用中使用多个EF Core DbContext本可行,但NotificationDbContext的实体依赖AppDbContext里的Employee表,存在关联关系,因此让NotificationDbContext继承了AppDbContext。执行迁移命令:
dotnet ef database update --project ... --startup-project ... --context NotificationDbContext
时出现错误:
数据库中已存在名为“Transactions.ContractSequence”的对象。
原因是继承导致EF Core尝试运行两个上下文的迁移脚本,求解决办法。
相关代码:
public sealed class NotificationDbContext : AppDbContext { public NotificationDbContext(DbContextOptions<AppDbContext> options, ITenancyContext<ApplicationTenant> tenancyContext, ILogger<AppDbContext> logger, CurrentUser userEmail) : base(options, tenancyContext, logger, userEmail) { } public DbSet<EventType> EventTypes => Set<EventType>(); public DbSet<Sound> Sounds => Set<Sound>(); public DbSet<Notification> Notifications => Set<Notification>(); public DbSet<UserNotificationSettings> UserNotificationSettings => Set<UserNotificationSettings>(); public DbSet<GlobalNotificationSettings> GlobalNotificationSettings => Set<GlobalNotificationSettings>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var targetEntityTypes = Assembly.GetExecutingAssembly().GetTypes() .Where(t => typeof(ITenantable).IsAssignableFrom(t) && t is { IsClass: true, IsAbstract: false }) .ToList(); foreach (var entityType in targetEntityTypes) { modelBuilder.HasTenancy(entityType, () => TenantId, TenancyModelState, hasIndex: false); } modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } }
public class AppDbContext : DbContext, ITenantDbContext<ApplicationTenant, Guid> { static TenancyModelState<Guid> _tenancyModelState; readonly ITenancyContext<ApplicationTenant> _tenancyContext; readonly ILogger _logger; readonly string _userEmail; public AppDbContext(DbContextOptions<AppDbContext> options, ITenancyContext<ApplicationTenant> tenancyContext, ILogger<AppDbContext> logger, CurrentUser userEmail) : base(options) { _tenancyContext = tenancyContext; _userEmail = userEmail?.Email ?? CurrentUser.InternalUser; _logger = logger; } public Guid TenantId => _tenancyContext.Tenant.Id; public static TenancyModelState<Guid> TenancyModelState => _tenancyModelState; public DbSet<ApplicationTenant> Tenants { get; set; } public DbSet<InstrumentView> InstrumentView { get; set; } public DbSet<vwNoAdminEmployees> NoAdminEmployees { get; set; } public DbSet<TenantView> TenantView { get; set; } public DbSet<LocalMonitoredSymbols> LocalMonitoredSymbolsView { get; set; } public DbSet<GlobexSchedule> GlobexSchedules { get; set; } public DbSet<Contract> Contracts { get; set; } public DbSet<Employee> Employees { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var tenantStoreOptions = new TenantStoreOptions(); modelBuilder.ConfigureTenantContext<ApplicationTenant, Guid>(tenantStoreOptions); // Add multi-tenancy support to model. var tenantReferenceOptions = new TenantReferenceOptions(); modelBuilder.HasTenancy(tenantReferenceOptions, out _tenancyModelState); modelBuilder.Entity<ApplicationTenant>(b => { b.Property(t => t.DisplayName).HasMaxLength(256); }); modelBuilder.HasSequence<int>("ContractSequence", "Transactions") .StartsAt(1000) .IncrementsBy(1); modelBuilder.Entity<ApplicationTenant>().ToTable("Tenant", "Security"); var targetEntityTypes = typeof(ITenantable).Assembly.GetTypes() .Where(t => typeof(ITenantable).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract) .ToList(); foreach (var entityType in targetEntityTypes) { modelBuilder.HasTenancy(entityType, () => _tenancyContext.Tenant.Id, _tenancyModelState, hasIndex: false); } modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } public override int SaveChanges(bool acceptAllChangesOnSuccess) { // Ensure multi-tenancy for all tenantable entities. this.EnsureTenancy(_tenancyContext?.Tenant?.Id, _tenancyModelState, _logger); return base.SaveChanges(acceptAllChangesOnSuccess); } void SaveAuthInfo() { // get entries that are being Added or Updated to add the created and updated information var modifiedEntries = ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified).ToList(); for (var i = modifiedEntries.Count - 1; i >= 0; i--) { var entry = modifiedEntries[i]; if (entry.Entity is not Entity entity) continue; if (entry.Entity is not IAuditable) { if (entry.State == EntityState.Added) { entity.CreateEntity(_userEmail); } continue; } if (entry.State == EntityState.Added) { entity.CreateEntity(_userEmail); } else if (entry.State == EntityState.Modified) { entity.UpdateEntity(_userEmail); } } } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { SaveAuthInfo(); // Ensure multi-tenancy for all tenantable entities. this.EnsureTenancy(_tenancyContext?.Tenant?.Id, _tenancyModelState, _logger); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } public override int SaveChanges() { SaveAuthInfo(); return base.SaveChanges(); } public DbConnection GetDbConnection() => Database.GetDbConnection(); public async Task ExecuteSpAndRead(string sql, IList<IDataParameter> parameters, Action<IDataReader> fnItem) { var dbConnection = Database.GetDbConnection(); await dbConnection.OpenAsync(); using IDbCommand command = dbConnection.CreateCommand(); command.CommandText = sql; command.CommandType = CommandType.StoredProcedure; if (parameters != null) { foreach (var param in parameters) { command.Parameters.Add(param); } } using var reader = command.ExecuteReader(); while (reader.Read()) { fnItem(reader); } } public async Task ExecuteQueryAndRead(string sql, IList<IDataParameter> parameters, Action<IDataReader> fnItem) { var dbConnection = Database.GetDbConnection(); await dbConnection.OpenAsync(); using IDbCommand command = dbConnection.CreateCommand(); command.CommandText = sql; command.CommandType = CommandType.Text; if (parameters != null) { foreach (var param in parameters) { command.Parameters.Add(param); } } using var reader = command.ExecuteReader(); while (reader.Read()) { fnItem(reader); } } public async Task ExecuteQuery(string sql, IList<IDataParameter> parameters) { await using var dbConnection = Database.GetDbConnection(); await dbConnection.OpenAsync(); using IDbCommand command = dbConnection.CreateCommand(); command.CommandText = sql; command.CommandType = CommandType.Text; if (parameters != null) { foreach (var param in parameters) { command.Parameters.Add(param); } } command.ExecuteNonQuery(); } }
解决方案
不要让NotificationDbContext继承AppDbContext,改用独立上下文+映射现有表的方式处理跨上下文关联,具体步骤如下:
1. 重构NotificationDbContext为独立上下文
让它直接继承DbContext,仅包含通知相关实体;通过创建EmployeeStub类映射现有Employee表,实现关联逻辑:
public sealed class NotificationDbContext : DbContext { private readonly Guid _tenantId; private readonly TenancyModelState<Guid> _tenancyModelState; private readonly ILogger _logger; private readonly string _userEmail; public NotificationDbContext(DbContextOptions<NotificationDbContext> options, ITenancyContext<ApplicationTenant> tenancyContext, ILogger<NotificationDbContext> logger, CurrentUser userEmail) : base(options) { _tenantId = tenancyContext.Tenant.Id; _tenancyModelState = AppDbContext.TenancyModelState; _logger = logger; _userEmail = userEmail?.Email ?? CurrentUser.InternalUser; } public DbSet<EventType> EventTypes => Set<EventType>(); public DbSet<Sound> Sounds => Set<Sound>(); public DbSet<Notification> Notifications => Set<Notification>(); public DbSet<UserNotificationSettings> UserNotificationSettings => Set<UserNotificationSettings>(); public DbSet<GlobalNotificationSettings> GlobalNotificationSettings => Set<GlobalNotificationSettings>(); // 映射现有Employee表的Stub类 public DbSet<EmployeeStub> Employees => Set<EmployeeStub>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 配置EmployeeStub映射到数据库中已有的Employees表 modelBuilder.Entity<EmployeeStub>(b => { b.ToTable("Employees"); b.HasKey(e => e.Id); b.Property(e => e.Id).IsRequired(); // 仅保留关联所需字段,无需完整属性 b.Property(e => e.Email).IsRequired().HasMaxLength(256); b.Property(e => e.TenantId).IsRequired(); }); // 配置Notification与EmployeeStub的关联关系 modelBuilder.Entity<Notification>(b => { b.HasOne<EmployeeStub>() .WithMany() .HasForeignKey(n => n.EmployeeId) .OnDelete(DeleteBehavior.Restrict); }); // 复用多租户配置逻辑 var targetEntityTypes = Assembly.GetExecutingAssembly().GetTypes() .Where(t => typeof(ITenantable).IsAssignableFrom(t) && t is { IsClass: true, IsAbstract: false }) .ToList(); foreach (var entityType in targetEntityTypes) { modelBuilder.HasTenancy(entityType, () => _tenantId, _tenancyModelState, hasIndex: false); } modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } // 复制AppDbContext中必要的SaveChanges逻辑(多租户、审计) public override int SaveChanges(bool acceptAllChangesOnSuccess) { this.EnsureTenancy(_tenantId, _tenancyModelState, _logger); SaveAuthInfo(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { SaveAuthInfo(); this.EnsureTenancy(_tenantId, _tenancyModelState, _logger); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void SaveAuthInfo() { var modifiedEntries = ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified).ToList(); for (var i = modifiedEntries.Count - 1; i >= 0; i--) { var entry = modifiedEntries[i]; if (entry.Entity is not Entity entity) continue; if (entry.Entity is not IAuditable) { if (entry.State == EntityState.Added) { entity.CreateEntity(_userEmail); } continue; } if (entry.State == EntityState.Added) { entity.CreateEntity(_userEmail); } else if (entry.State == EntityState.Modified) { entity.UpdateEntity(_userEmail); } } } } // 仅用于关联的Employee Stub类 public class EmployeeStub : Entity, ITenantable { public string Email { get; set; } public Guid TenantId { get; set; } }
2. 为NotificationDbContext单独创建迁移
生成仅包含通知相关实体的迁移,避免重复创建AppDbContext已有的数据库对象:
dotnet ef migrations add InitialNotificationSchema --project 你的通知类库项目名 --startup-project 你的启动项目名 --context NotificationDbContext
执行迁移更新数据库:
dotnet ef database update --project 你的通知类库项目名 --startup-project 你的启动项目名 --context NotificationDbContext
3. 注册两个上下文到DI容器
在Startup/Program.cs中分别注册两个上下文,确保使用相同的数据库连接字符串:
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddDbContext<NotificationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
4. 注意事项
- 跨上下文的事务需使用
IDbContextTransactionFactory或TransactionScope保证数据一致性 EmployeeStub仅保留关联必需字段,不要包含业务逻辑;如需查询完整Employee数据,使用AppDbContext- 后续维护时,两个上下文的迁移需分别执行,避免互相影响
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

