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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 05:57:04