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();
}
}