using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; using API.Entities.Interfaces; using API.Entities.Metadata; using API.Entities.Scrobble; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace API.Data; public sealed class DataContext : IdentityDbContext, AppUserRole, IdentityUserLogin, IdentityRoleClaim, IdentityUserToken> { public DataContext(DbContextOptions options) : base(options) { ChangeTracker.Tracked += OnEntityTracked; ChangeTracker.StateChanged += OnEntityStateChanged; } public DbSet Library { get; set; } = null!; public DbSet Series { get; set; } = null!; public DbSet Chapter { get; set; } = null!; public DbSet Volume { get; set; } = null!; public DbSet AppUser { get; set; } = null!; public DbSet MangaFile { get; set; } = null!; public DbSet AppUserProgresses { get; set; } = null!; public DbSet AppUserRating { get; set; } = null!; public DbSet ServerSetting { get; set; } = null!; public DbSet AppUserPreferences { get; set; } = null!; public DbSet SeriesMetadata { get; set; } = null!; [Obsolete] public DbSet CollectionTag { get; set; } = null!; public DbSet AppUserBookmark { get; set; } = null!; public DbSet ReadingList { get; set; } = null!; public DbSet ReadingListItem { get; set; } = null!; public DbSet Person { get; set; } = null!; public DbSet Genre { get; set; } = null!; public DbSet Tag { get; set; } = null!; public DbSet SiteTheme { get; set; } = null!; public DbSet SeriesRelation { get; set; } = null!; public DbSet FolderPath { get; set; } = null!; public DbSet Device { get; set; } = null!; public DbSet ServerStatistics { get; set; } = null!; public DbSet MediaError { get; set; } = null!; public DbSet ScrobbleEvent { get; set; } = null!; public DbSet ScrobbleError { get; set; } = null!; public DbSet ScrobbleHold { get; set; } = null!; public DbSet AppUserOnDeckRemoval { get; set; } = null!; public DbSet AppUserTableOfContent { get; set; } = null!; public DbSet AppUserSmartFilter { get; set; } = null!; public DbSet AppUserDashboardStream { get; set; } = null!; public DbSet AppUserSideNavStream { get; set; } = null!; public DbSet AppUserExternalSource { get; set; } = null!; public DbSet ExternalReview { get; set; } = null!; public DbSet ExternalRating { get; set; } = null!; public DbSet ExternalSeriesMetadata { get; set; } = null!; public DbSet ExternalRecommendation { get; set; } = null!; public DbSet ManualMigrationHistory { get; set; } = null!; public DbSet SeriesBlacklist { get; set; } = null!; public DbSet AppUserCollection { get; set; } = null!; public DbSet ChapterPeople { get; set; } = null!; public DbSet SeriesMetadataPeople { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity() .HasMany(ur => ur.UserRoles) .WithOne(u => u.User) .HasForeignKey(ur => ur.UserId) .IsRequired(); builder.Entity() .HasMany(ur => ur.UserRoles) .WithOne(u => u.Role) .HasForeignKey(ur => ur.RoleId) .IsRequired(); builder.Entity() .HasOne(pt => pt.Series) .WithMany(p => p.Relations) .HasForeignKey(pt => pt.SeriesId) .OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasOne(pt => pt.TargetSeries) .WithMany(t => t.RelationOf) .HasForeignKey(pt => pt.TargetSeriesId) .OnDelete(DeleteBehavior.Cascade); builder.Entity() .Property(b => b.BookThemeName) .HasDefaultValue("Dark"); builder.Entity() .Property(b => b.BackgroundColor) .HasDefaultValue("#000000"); builder.Entity() .Property(b => b.GlobalPageLayoutMode) .HasDefaultValue(PageLayoutMode.Cards); builder.Entity() .Property(b => b.BookReaderWritingStyle) .HasDefaultValue(WritingStyle.Horizontal); builder.Entity() .Property(b => b.Locale) .IsRequired(true) .HasDefaultValue("en"); builder.Entity() .Property(b => b.AllowScrobbling) .HasDefaultValue(true); builder.Entity() .Property(b => b.WebLinks) .HasDefaultValue(string.Empty); builder.Entity() .Property(b => b.WebLinks) .HasDefaultValue(string.Empty); builder.Entity() .Property(b => b.ISBN) .HasDefaultValue(string.Empty); builder.Entity() .Property(b => b.StreamType) .HasDefaultValue(DashboardStreamType.SmartFilter); builder.Entity() .HasIndex(e => e.Visible) .IsUnique(false); builder.Entity() .Property(b => b.StreamType) .HasDefaultValue(SideNavStreamType.SmartFilter); builder.Entity() .HasIndex(e => e.Visible) .IsUnique(false); builder.Entity() .HasOne(em => em.Series) .WithOne(s => s.ExternalSeriesMetadata) .HasForeignKey(em => em.SeriesId) .OnDelete(DeleteBehavior.Cascade); builder.Entity() .Property(b => b.AgeRating) .HasDefaultValue(AgeRating.Unknown); // Configure the many-to-many relationship for Movie and Person builder.Entity() .HasKey(cp => new { cp.ChapterId, cp.PersonId, cp.Role }); builder.Entity() .HasOne(cp => cp.Chapter) .WithMany(c => c.People) .HasForeignKey(cp => cp.ChapterId); builder.Entity() .HasOne(cp => cp.Person) .WithMany(p => p.ChapterPeople) .HasForeignKey(cp => cp.PersonId) .OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasKey(smp => new { smp.SeriesMetadataId, smp.PersonId, smp.Role }); builder.Entity() .HasOne(smp => smp.SeriesMetadata) .WithMany(sm => sm.People) .HasForeignKey(smp => smp.SeriesMetadataId); builder.Entity() .HasOne(smp => smp.Person) .WithMany(p => p.SeriesMetadataPeople) .HasForeignKey(smp => smp.PersonId) .OnDelete(DeleteBehavior.Cascade); } #nullable enable private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e) { if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return; entity.LastModified = DateTime.Now; entity.LastModifiedUtc = DateTime.UtcNow; // This allows for mocking if (entity.Created == DateTime.MinValue) { entity.Created = DateTime.Now; entity.CreatedUtc = DateTime.UtcNow; } } private static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e) { if (e.NewState != EntityState.Modified || e.Entry.Entity is not IEntityDate entity) return; entity.LastModified = DateTime.Now; entity.LastModifiedUtc = DateTime.UtcNow; } #nullable disable private void OnSaveChanges() { foreach (var saveEntity in ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .Select(entry => entry.Entity) .OfType()) { saveEntity.OnSavingChanges(); } } #region SaveChanges overrides public override int SaveChanges() { OnSaveChanges(); return base.SaveChanges(); } public override int SaveChanges(bool acceptAllChangesOnSuccess) { OnSaveChanges(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { OnSaveChanges(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } public override Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) { OnSaveChanges(); return base.SaveChangesAsync(cancellationToken); } #endregion }