1

I wanted to split that notification feature into a separate Class Library project and have a separate DB context. Multiple EF Core DB contexts should be just fine. The problem is that my entities in NotificationDbContext depend on the Employee table that comes from AppDbContext. They have relationships between them. That's why I inherit NotificationDbContext from AppDbContext but the real problem is when I run dotnet ef database update --project ... --startup-project ... --context NotificationDbContext.

There is already an object named 'Transactions.ContractSequence' in the database.

That's kinda normal, because it's trying to run the migration scripts of both contexts because of the inheritance. How can I deal with it?

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();
    }
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • 1
    When using bounded contexts and considering Migrations then only one DbContext definition should be Code-First while the others are set up for DB-First. (No migrations) If you want each DbContext to be responsible for migrations of particular tables then you may be able to exclude specific entities from migration via `ExcludeFromMigrations` (EF Core 5+): https://stackoverflow.com/questions/22038924/how-to-exclude-one-table-from-automatic-code-first-migrations-in-the-entity-fram – Steve Py Mar 29 '23 at 01:18

0 Answers0