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