using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using System.Reflection; using Tiku.Application.Security; using Tiku.Domain.Catalog; using Tiku.Domain.Commerce; using Tiku.Domain.Common; using Tiku.Domain.Content; using Tiku.Domain.Growth; using Tiku.Domain.Identity; using Tiku.Domain.Import; using Tiku.Domain.Learning; using Tiku.Domain.Operations; using Tiku.Domain.Platform; using Tiku.Domain.QuestionBanks; using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Persistence; public sealed class TikuDbContext( DbContextOptions options, ITenantContext tenantContext) : IdentityUserContext(options), IDataProtectionKeyContext { public TikuDbContext(DbContextOptions options) : this(options, CreateToolingTenantContext()) { } public Guid? CurrentTenantId => tenantContext.TenantId; public Guid CurrentTenantIdOrEmpty => tenantContext.TenantId ?? Guid.Empty; public bool IsTenantResolved => tenantContext.IsResolved; public bool IsSystemScope => tenantContext.IsSystem; private static ITenantContext CreateToolingTenantContext() { var context = new TenantContext(); context.InitializeSystem(null, "Direct DbContext construction for model tooling"); return context; } public DbSet Tenants => Set(); public new DbSet Users => Set(); public DbSet DataProtectionKeys => Set(); public DbSet UserIdentities => Set(); public DbSet TenantMemberships => Set(); public DbSet TenantDomains => Set(); public DbSet TenantBrandings => Set(); public DbSet TenantSettings => Set(); public DbSet TenantFrontendConfigs => Set(); public DbSet TenantExternalProviders => Set(); public DbSet TenantSecrets => Set(); public DbSet SmsVerificationCodes => Set(); public DbSet AuthLoginEvents => Set(); public DbSet AuthSessions => Set(); public DbSet AuthChallenges => Set(); public DbSet SmsSendRateLimits => Set(); public DbSet TenantClasses => Set(); public DbSet TenantClassMembers => Set(); public DbSet TenantStudentNotes => Set(); public DbSet TenantStudentFollowups => Set(); public DbSet StudentProfiles => Set(); public DbSet Regions => Set(); public DbSet RegionModules => Set(); public DbSet ModuleNodes => Set(); public DbSet Schools => Set(); public DbSet Majors => Set(); public DbSet Subjects => Set(); public DbSet Categories => Set(); public DbSet TaxonomyNodes => Set(); public DbSet QuestionTaxonomyAssignments => Set(); public DbSet ScorelineFields => Set(); public DbSet ScorelineRecords => Set(); public DbSet QuestionBanks => Set(); public DbSet Questions => Set(); public DbSet QuestionVersions => Set(); public DbSet ContentEntries => Set(); public DbSet ContentNodes => Set(); public DbSet QuestionCollections => Set(); public DbSet QuestionCollectionItems => Set(); public DbSet PracticeBlueprints => Set(); public DbSet VocabularyUnits => Set(); public DbSet VocabularyWords => Set(); public DbSet UserWordProgress => Set(); public DbSet UserWordFavorites => Set(); public DbSet HandbookSubjects => Set(); public DbSet HandbookChapters => Set(); public DbSet HandbookEntries => Set(); public DbSet QuestionTypeGroups => Set(); public DbSet SubjectShares => Set(); public DbSet ContentAssets => Set(); public DbSet ContentAssetAccessEvents => Set(); public DbSet ContentAssetSecurityScanEvents => Set(); public DbSet ContentImportJobs => Set(); public DbSet ContentImportItems => Set(); public DbSet ContentImportIssues => Set(); public DbSet Images => Set(); public DbSet AppAssets => Set(); public DbSet VideoExplanations => Set(); public DbSet QuestionVideos => Set(); public DbSet TenantQuestionBankPreferences => Set(); public DbSet TenantQuestionReferences => Set(); public DbSet AiRecommendationReports => Set(); public DbSet PracticeSessions => Set(); public DbSet PracticeSessionQuestions => Set(); public DbSet AnswerRecords => Set(); public DbSet FavoriteQuestions => Set(); public DbSet WrongQuestions => Set(); public DbSet RecentPractices => Set(); public DbSet ExamDates => Set(); public DbSet Reports => Set(); public DbSet ReportStatusEvents => Set(); public DbSet UserScoreEvents => Set(); public DbSet PracticeDailyUsages => Set(); public DbSet PracticeAccessEvents => Set(); public DbSet PracticeSessionReports => Set(); public DbSet PracticeSessionReportSections => Set(); public DbSet DashboardDailyStats => Set(); public DbSet RevenueDailyStats => Set(); public DbSet Products => Set(); public DbSet SvipPlans => Set(); public DbSet Orders => Set(); public DbSet OrderItems => Set(); public DbSet Payments => Set(); public DbSet PaymentEvents => Set(); public DbSet Entitlements => Set(); public DbSet CodeBatches => Set(); public DbSet ActivationCodes => Set(); public DbSet Coupons => Set(); public DbSet CouponRedemptions => Set(); public DbSet TenantSubscriptions => Set(); public DbSet TenantUsageRecords => Set(); public DbSet CommerceRefundRequests => Set(); public DbSet CommerceRefundEvents => Set(); public DbSet CommerceReconciliationBatches => Set(); public DbSet CommerceReconciliationItems => Set(); public DbSet CommerceReconciliationIssues => Set(); public DbSet CommerceReconciliationIssueEvents => Set(); public DbSet PointActivityTasks => Set(); public DbSet PointActivityClaims => Set(); public DbSet PointExchangeItems => Set(); public DbSet PointExchangeOrders => Set(); public DbSet ReferralTracks => Set(); public DbSet ReferralCodes => Set(); public DbSet ReferralLeads => Set(); public DbSet ReferralTeamEdges => Set(); public DbSet ReferralQrcodes => Set(); public DbSet CrmConfigs => Set(); public DbSet CrmWebhookQueue => Set(); public DbSet CrmWebhookLogs => Set(); public DbSet TenantCommissionSettings => Set(); public DbSet CommissionSettlements => Set(); public DbSet CommissionSettlementItems => Set(); public DbSet CommissionSettlementProofs => Set(); public DbSet CommissionSettlementExportEvents => Set(); public DbSet Banners => Set(); public DbSet Faqs => Set(); public DbSet Announcements => Set(); public DbSet AuditLogs => Set(); public DbSet BackendPermissions => Set(); public DbSet BackendMenus => Set(); public DbSet TenantBackendRoles => Set(); public DbSet TenantBackendRolePermissions => Set(); public DbSet TenantBackendRoleMenus => Set(); public DbSet TenantBackendUserRoles => Set(); public DbSet PlatformBackendRoles => Set(); public DbSet PlatformBackendRolePermissions => Set(); public DbSet PlatformBackendRoleMenus => Set(); public DbSet PlatformBackendUserRoles => Set(); public DbSet BackgroundJobs => Set(); public DbSet UserNotifications => Set(); public DbSet Badges => Set(); public DbSet UserBadges => Set(); public DbSet TenantContentNotifications => Set(); public DbSet TenantThemeTemplates => Set(); public DbSet TenantThemeConfigs => Set(); public DbSet PlatformSaasPlans => Set(); public DbSet TenantBillingProfiles => Set(); public DbSet TenantInvoices => Set(); public DbSet TenantInvoiceItems => Set(); public DbSet TenantInvoicePayments => Set(); public DbSet TenantInvoiceReminders => Set(); public DbSet PlatformAuditAlertRules => Set(); public DbSet PlatformAuditAlerts => Set(); public DbSet PlatformDunningNotificationChannels => Set(); public DbSet PlatformDunningNotificationEvents => Set(); public DbSet PocketBaseImportRuns => Set(); public DbSet PocketBaseRawRecords => Set(); public DbSet PocketBaseImportIssues => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity>().ToTable("user_claims"); modelBuilder.Entity>().ToTable("user_logins"); modelBuilder.Entity>().ToTable("user_tokens"); modelBuilder.HasPostgresExtension("citext"); modelBuilder.HasPostgresExtension("ltree"); modelBuilder.ApplyConfigurationsFromAssembly(typeof(TikuDbContext).Assembly); modelBuilder.Entity().ToTable("data_protection_keys"); ApplyTenantQueryFilters(modelBuilder); ValidateTenantModel(modelBuilder); modelBuilder.UseSnakeCaseIdentifiers(); } private void ApplyTenantQueryFilters(ModelBuilder modelBuilder) { var applyMethod = typeof(TikuDbContext) .GetMethod(nameof(ApplyTenantQueryFilter), BindingFlags.Instance | BindingFlags.NonPublic)!; foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var tenantProperty = entityType.FindProperty("TenantId"); if (tenantProperty?.ClrType != typeof(Guid) || entityType.BaseType is not null) { continue; } applyMethod.MakeGenericMethod(entityType.ClrType).Invoke(this, [modelBuilder]); } } private void ApplyTenantQueryFilter(ModelBuilder modelBuilder) where TEntity : class { modelBuilder.Entity().HasQueryFilter(entity => IsSystemScope || (IsTenantResolved && EF.Property(entity, "TenantId") == CurrentTenantIdOrEmpty)); } private static void ValidateTenantModel(ModelBuilder modelBuilder) { var invalidUniqueIndexes = new List(); var invalidTenantForeignKeys = new List(); foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var tenantProperty = entityType.FindProperty("TenantId"); if (tenantProperty?.ClrType != typeof(Guid)) { continue; } if (!typeof(ITenantOwned).IsAssignableFrom(entityType.ClrType)) { throw new InvalidOperationException( $"Tenant entity '{entityType.ClrType.Name}' must implement {nameof(ITenantOwned)}."); } if (!entityType.GetDeclaredQueryFilters().Any()) { throw new InvalidOperationException( $"Tenant entity '{entityType.ClrType.Name}' does not have a tenant query filter."); } foreach (var index in entityType.GetDeclaredIndexes().Where(index => index.IsUnique)) { if (index.Properties.All(property => property.Name != "TenantId") && index.FindAnnotation("Tiku:GlobalUnique")?.Value is not true) { invalidUniqueIndexes.Add( $"{entityType.ClrType.Name}({string.Join(",", index.Properties.Select(property => property.Name))})"); } } foreach (var foreignKey in entityType.GetDeclaredForeignKeys().Where(foreignKey => typeof(ITenantOwned).IsAssignableFrom(foreignKey.PrincipalEntityType.ClrType))) { if (foreignKey.PrincipalKey.Properties.All(property => property.Name != "TenantId")) { invalidTenantForeignKeys.Add( $"{entityType.ClrType.Name}->{foreignKey.PrincipalEntityType.ClrType.Name}"); } } } if (invalidUniqueIndexes.Count > 0) { throw new InvalidOperationException( $"Unique indexes on tenant entities must include TenantId: {string.Join("; ", invalidUniqueIndexes)}"); } if (invalidTenantForeignKeys.Count > 0) { throw new InvalidOperationException( $"Foreign keys between tenant entities must use a tenant-qualified principal key: {string.Join("; ", invalidTenantForeignKeys)}"); } } public override int SaveChanges(bool acceptAllChangesOnSuccess) { UpdateTimestamps(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task SaveChangesAsync( bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { UpdateTimestamps(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void UpdateTimestamps() { var now = DateTimeOffset.UtcNow; foreach (var entry in ChangeTracker.Entries().Where(entry => entry.State == EntityState.Modified)) { if (entry.Property(user => user.Status).IsModified || entry.Property(user => user.PasswordHash).IsModified) { entry.Entity.SecurityStamp = Guid.NewGuid().ToString("N"); } } foreach (var entry in ChangeTracker.Entries()) { if (entry.State == EntityState.Added) { entry.Entity.CreatedAt = now; } if (entry.State is EntityState.Added or EntityState.Modified) { entry.Entity.UpdatedAt = now; } } } }