using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SVSim.Database.Common; using SVSim.Database.DataSeeders; using SVSim.Database.Models; namespace SVSim.Database; public class SVSimDbContext : DbContext { private readonly ILogger _logger; public SVSimDbContext(ILogger logger, DbContextOptions options) : base(options) { _logger = logger; } #region DbSets public DbSet Viewers => Set(); public DbSet Cards => Set(); public DbSet CardSets => Set(); public DbSet Decks => Set(); public DbSet Classes => Set(); public DbSet ClassExpCurve => Set(); public DbSet LeaderSkins => Set(); public DbSet Sleeves => Set(); public DbSet Emblems => Set(); public DbSet Degrees => Set(); public DbSet MyPageBackgrounds => Set(); public DbSet Battlefields => Set(); public DbSet RankInfo => Set(); public DbSet Items => Set(); public DbSet GameConfigurations => Set(); // Prod-captured globals — populated by SVSim.Bootstrap, not HasData. See // docs/audits/prod-data-capture-strategy-2026-05-23.md. public DbSet MyRotationSettings => Set(); public DbSet MyRotationAbilities => Set(); public DbSet AvatarAbilities => Set(); public DbSet DefaultDecks => Set(); public DbSet DefaultLeaderSkinSettings => Set(); public DbSet ArenaSeasons => Set(); public DbSet SpotCards => Set(); public DbSet ReprintedCards => Set(); public DbSet UnlimitedRestrictions => Set(); public DbSet LoadingExclusionCards => Set(); public DbSet BattlePassLevels => Set(); public DbSet DailyLoginBonuses => Set(); public DbSet Banners => Set(); public DbSet Colosseums => Set(); public DbSet SealedSeasons => Set(); public DbSet MasterPointRankingPeriods => Set(); public DbSet MaintenanceCards => Set(); public DbSet FeatureMaintenances => Set(); public DbSet PreReleaseInfos => Set(); #endregion public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { foreach (var entityEntry in ChangeTracker.Entries()) { if (entityEntry.Entity is ITimeTrackedEntity timeTrackedEntity) { if (entityEntry.State is EntityState.Added && timeTrackedEntity.DateCreated == DateTime.MinValue) { timeTrackedEntity.DateCreated = DateTime.UtcNow; } if (entityEntry.State is EntityState.Modified or EntityState.Added) { timeTrackedEntity.DateUpdated = DateTime.UtcNow; } } } return await base.SaveChangesAsync(cancellationToken); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .OwnsMany(de => de.Cards); // BaseEntity annotates Id with [DatabaseGenerated(None)] for the integer-PK // entities seeded via HasData. ShadowverseDeckEntry uses Guid and is created at // runtime — without client-side generation every new deck gets Guid.Empty and the // second deck insert collides on PK. (DDL has no column default; this only works // because EF generates a sequential Guid before INSERT.) modelBuilder.Entity() .Property(d => d.Id) .ValueGeneratedOnAdd(); // EF can't figure this many-to-many out on its own modelBuilder.Entity() .HasMany(se => se.Viewers) .WithMany(v => v.Sleeves); modelBuilder.HasSequence("ShortUdidSequence").StartsAt(400000000); modelBuilder.Entity() .Property(v => v.ShortUdid) .UseSequence("ShortUdidSequence"); new BaseDataSeeder().Seed(modelBuilder); new DefaultSettingsSeeder().Seed(modelBuilder); base.OnModelCreating(modelBuilder); } public void UpdateDatabase() { IEnumerable pendingMigrations = Database.GetPendingMigrations(); if (!pendingMigrations.Any()) { _logger.LogDebug("No pending migrations found, continuing."); return; } foreach (string migration in pendingMigrations) { _logger.LogInformation("Found pending migration with name {migrationName}.", migration); } _logger.LogInformation("Attempting to apply pending migrations..."); Database.Migrate(); _logger.LogInformation("Migrations applied."); } }