< Summary

Information
Class: Chronicis.Api.Data.ChronicisDbContext
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Data/ChronicisDbContext.cs
Line coverage
100%
Covered lines: 722
Uncovered lines: 0
Coverable lines: 722
Total lines: 798
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Data/ChronicisDbContext.cs

#LineLine coverage
 1using Chronicis.Shared.Models;
 2using Microsoft.EntityFrameworkCore;
 3
 4namespace Chronicis.Api.Data;
 5
 6/// <summary>
 7/// Database context for Chronicis application.
 8/// </summary>
 9public class ChronicisDbContext : DbContext
 10{
 11    // ===== Core Entities =====
 55312    public DbSet<User> Users { get; set; } = null!;
 47813    public DbSet<World> Worlds { get; set; } = null!;
 59314    public DbSet<WorldMember> WorldMembers { get; set; } = null!;
 27615    public DbSet<WorldInvitation> WorldInvitations { get; set; } = null!;
 39716    public DbSet<Campaign> Campaigns { get; set; } = null!;
 39017    public DbSet<Arc> Arcs { get; set; } = null!;
 53318    public DbSet<Article> Articles { get; set; } = null!;
 22819    public DbSet<ArticleAlias> ArticleAliases { get; set; } = null!;
 27120    public DbSet<ArticleLink> ArticleLinks { get; set; } = null!;
 22821    public DbSet<ArticleExternalLink> ArticleExternalLinks { get; set; } = null!;
 22822    public DbSet<WorldLink> WorldLinks { get; set; } = null!;
 22823    public DbSet<WorldDocument> WorldDocuments { get; set; } = null!;
 22824    public DbSet<SummaryTemplate> SummaryTemplates { get; set; } = null!;
 22825    public DbSet<ResourceProvider> ResourceProviders { get; set; } = null!;
 22826    public DbSet<WorldResourceProvider> WorldResourceProviders { get; set; } = null!;
 29327    public DbSet<Quest> Quests { get; set; } = null!;
 26828    public DbSet<QuestUpdate> QuestUpdates { get; set; } = null!;
 29
 30
 31    public ChronicisDbContext(DbContextOptions<ChronicisDbContext> options)
 22832        : base(options)
 33    {
 22834    }
 35
 36    protected override void OnModelCreating(ModelBuilder modelBuilder)
 37    {
 138        ConfigureUser(modelBuilder);
 139        ConfigureWorld(modelBuilder);
 140        ConfigureWorldMember(modelBuilder);
 141        ConfigureWorldInvitation(modelBuilder);
 142        ConfigureCampaign(modelBuilder);
 143        ConfigureArc(modelBuilder);
 144        ConfigureArticle(modelBuilder);
 145        ConfigureArticleAlias(modelBuilder);
 146        ConfigureArticleLink(modelBuilder);
 147        ConfigureArticleExternalLink(modelBuilder);
 148        ConfigureWorldLink(modelBuilder);
 149        ConfigureWorldDocument(modelBuilder);
 150        ConfigureSummaryTemplate(modelBuilder);
 151        ConfigureResourceProvider(modelBuilder);
 152        ConfigureWorldResourceProvider(modelBuilder);
 153        ConfigureQuest(modelBuilder);
 154        ConfigureQuestUpdate(modelBuilder);
 155    }
 56
 57    private static void ConfigureUser(ModelBuilder modelBuilder)
 58    {
 159        modelBuilder.Entity<User>(entity =>
 160        {
 161            entity.HasKey(u => u.Id);
 162
 163            entity.Property(u => u.Auth0UserId)
 164                .HasMaxLength(256)
 165                .IsRequired();
 166
 167            entity.Property(u => u.Email)
 168                .HasMaxLength(256)
 169                .IsRequired();
 170
 171            entity.Property(u => u.DisplayName)
 172                .HasMaxLength(100)
 173                .IsRequired();
 174
 175            entity.Property(u => u.AvatarUrl)
 176                .HasMaxLength(500);
 177
 178            entity.HasIndex(u => u.Auth0UserId)
 179                .IsUnique();
 180
 181            entity.HasIndex(u => u.Email);
 282        });
 183    }
 84
 85    private static void ConfigureWorld(ModelBuilder modelBuilder)
 86    {
 187        modelBuilder.Entity<World>(entity =>
 188        {
 189            entity.HasKey(w => w.Id);
 190
 191            entity.Property(w => w.Name)
 192                .HasMaxLength(200)
 193                .IsRequired();
 194
 195            entity.Property(w => w.Slug)
 196                .HasMaxLength(200)
 197                .IsRequired();
 198
 199            entity.Property(w => w.Description)
 1100                .HasMaxLength(1000);
 1101
 1102            // Public access fields
 1103            entity.Property(w => w.IsPublic)
 1104                .HasDefaultValue(false);
 1105
 1106            entity.Property(w => w.PublicSlug)
 1107                .HasMaxLength(100);
 1108
 1109            // World -> Owner (User)
 1110            entity.HasOne(w => w.Owner)
 1111                .WithMany(u => u.OwnedWorlds)
 1112                .HasForeignKey(w => w.OwnerId)
 1113                .OnDelete(DeleteBehavior.Restrict);
 1114
 1115            entity.HasIndex(w => w.OwnerId);
 1116
 1117            // Unique constraint: Slug must be unique per owner
 1118            entity.HasIndex(w => new { w.OwnerId, w.Slug })
 1119                .IsUnique()
 1120                .HasDatabaseName("IX_Worlds_OwnerId_Slug");
 1121
 1122            // Unique constraint: PublicSlug must be globally unique (when not null)
 1123            entity.HasIndex(w => w.PublicSlug)
 1124                .IsUnique()
 1125                .HasFilter("[PublicSlug] IS NOT NULL")
 1126                .HasDatabaseName("IX_Worlds_PublicSlug");
 2127        });
 1128    }
 129
 130    private static void ConfigureCampaign(ModelBuilder modelBuilder)
 131    {
 1132        modelBuilder.Entity<Campaign>(entity =>
 1133        {
 1134            entity.HasKey(c => c.Id);
 1135
 1136            entity.Property(c => c.Name)
 1137                .HasMaxLength(200)
 1138                .IsRequired();
 1139
 1140            entity.Property(c => c.Description)
 1141                .HasMaxLength(1000);
 1142
 1143            // Campaign -> World
 1144            entity.HasOne(c => c.World)
 1145                .WithMany(w => w.Campaigns)
 1146                .HasForeignKey(c => c.WorldId)
 1147                .OnDelete(DeleteBehavior.Restrict);
 1148
 1149            // Campaign -> Owner (User)
 1150            entity.HasOne(c => c.Owner)
 1151                .WithMany(u => u.OwnedCampaigns)
 1152                .HasForeignKey(c => c.OwnerId)
 1153                .OnDelete(DeleteBehavior.Restrict);
 1154
 1155            entity.HasIndex(c => c.WorldId);
 1156            entity.HasIndex(c => c.OwnerId);
 2157        });
 1158    }
 159
 160    private static void ConfigureWorldMember(ModelBuilder modelBuilder)
 161    {
 1162        modelBuilder.Entity<WorldMember>(entity =>
 1163        {
 1164            entity.HasKey(wm => wm.Id);
 1165
 1166            // WorldMember -> World
 1167            entity.HasOne(wm => wm.World)
 1168                .WithMany(w => w.Members)
 1169                .HasForeignKey(wm => wm.WorldId)
 1170                .OnDelete(DeleteBehavior.Cascade);
 1171
 1172            // WorldMember -> User
 1173            entity.HasOne(wm => wm.User)
 1174                .WithMany(u => u.WorldMemberships)
 1175                .HasForeignKey(wm => wm.UserId)
 1176                .OnDelete(DeleteBehavior.Cascade);
 1177
 1178            // WorldMember -> Inviter (User)
 1179            entity.HasOne(wm => wm.Inviter)
 1180                .WithMany(u => u.InvitedMembers)
 1181                .HasForeignKey(wm => wm.InvitedBy)
 1182                .OnDelete(DeleteBehavior.Restrict);
 1183
 1184            // Unique constraint: One membership per user per world
 1185            entity.HasIndex(wm => new { wm.WorldId, wm.UserId })
 1186                .IsUnique();
 2187        });
 1188    }
 189
 190    private static void ConfigureWorldInvitation(ModelBuilder modelBuilder)
 191    {
 1192        modelBuilder.Entity<WorldInvitation>(entity =>
 1193        {
 1194            entity.HasKey(wi => wi.Id);
 1195
 1196            entity.Property(wi => wi.Code)
 1197                .HasMaxLength(9) // XXXX-XXXX format
 1198                .IsRequired();
 1199
 1200            // WorldInvitation -> World
 1201            entity.HasOne(wi => wi.World)
 1202                .WithMany(w => w.Invitations)
 1203                .HasForeignKey(wi => wi.WorldId)
 1204                .OnDelete(DeleteBehavior.Cascade);
 1205
 1206            // WorldInvitation -> Creator (User)
 1207            entity.HasOne(wi => wi.Creator)
 1208                .WithMany(u => u.CreatedInvitations)
 1209                .HasForeignKey(wi => wi.CreatedBy)
 1210                .OnDelete(DeleteBehavior.Restrict);
 1211
 1212            // Unique constraint: Invitation codes must be globally unique
 1213            entity.HasIndex(wi => wi.Code)
 1214                .IsUnique();
 1215
 1216            // Index for looking up active invitations by world
 1217            entity.HasIndex(wi => new { wi.WorldId, wi.IsActive });
 2218        });
 1219    }
 220
 221    private static void ConfigureArc(ModelBuilder modelBuilder)
 222    {
 1223        modelBuilder.Entity<Arc>(entity =>
 1224        {
 1225            entity.HasKey(a => a.Id);
 1226
 1227            entity.Property(a => a.Name)
 1228                .HasMaxLength(200)
 1229                .IsRequired();
 1230
 1231            entity.Property(a => a.Description)
 1232                .HasMaxLength(1000);
 1233
 1234            // Arc -> Campaign
 1235            entity.HasOne(a => a.Campaign)
 1236                .WithMany(c => c.Arcs)
 1237                .HasForeignKey(a => a.CampaignId)
 1238                .OnDelete(DeleteBehavior.Cascade);
 1239
 1240            // Arc -> Creator (User)
 1241            entity.HasOne(a => a.Creator)
 1242                .WithMany(u => u.CreatedArcs)
 1243                .HasForeignKey(a => a.CreatedBy)
 1244                .OnDelete(DeleteBehavior.Restrict);
 1245
 1246            // Indexes
 1247            entity.HasIndex(a => a.CampaignId);
 1248            entity.HasIndex(a => a.CreatedBy);
 1249            entity.HasIndex(a => new { a.CampaignId, a.SortOrder });
 2250        });
 1251    }
 252
 253    private static void ConfigureArticle(ModelBuilder modelBuilder)
 254    {
 1255        modelBuilder.Entity<Article>(entity =>
 1256        {
 1257            entity.HasKey(a => a.Id);
 1258
 1259            // Content fields
 1260            entity.Property(a => a.Title)
 1261                .HasMaxLength(500);
 1262
 1263            entity.Property(a => a.Slug)
 1264                .HasMaxLength(200)
 1265                .IsRequired();
 1266
 1267            entity.Property(a => a.IconEmoji)
 1268                .HasMaxLength(50);
 1269
 1270            entity.Property(a => a.InGameDate)
 1271                .HasMaxLength(100);
 1272
 1273            // Self-referencing hierarchy
 1274            entity.HasOne(a => a.Parent)
 1275                .WithMany(a => a.Children)
 1276                .HasForeignKey(a => a.ParentId)
 1277                .OnDelete(DeleteBehavior.Restrict);
 1278
 1279            // Article -> World
 1280            entity.HasOne(a => a.World)
 1281                .WithMany(w => w.Articles)
 1282                .HasForeignKey(a => a.WorldId)
 1283                .OnDelete(DeleteBehavior.Restrict);
 1284
 1285            // Article -> Campaign
 1286            entity.HasOne(a => a.Campaign)
 1287                .WithMany(c => c.Articles)
 1288                .HasForeignKey(a => a.CampaignId)
 1289                .OnDelete(DeleteBehavior.Restrict);
 1290
 1291            // Article -> Arc (for Session articles)
 1292            entity.HasOne(a => a.Arc)
 1293                .WithMany(arc => arc.Sessions)
 1294                .HasForeignKey(a => a.ArcId)
 1295                .OnDelete(DeleteBehavior.Restrict);
 1296
 1297            // Article -> Creator (User)
 1298            entity.HasOne(a => a.Creator)
 1299                .WithMany(u => u.CreatedArticles)
 1300                .HasForeignKey(a => a.CreatedBy)
 1301                .OnDelete(DeleteBehavior.Restrict);
 1302
 1303            // Article -> Modifier (User)
 1304            entity.HasOne(a => a.Modifier)
 1305                .WithMany(u => u.ModifiedArticles)
 1306                .HasForeignKey(a => a.LastModifiedBy)
 1307                .OnDelete(DeleteBehavior.Restrict);
 1308
 1309            // Article -> Player (User) for Character ownership
 1310            entity.HasOne(a => a.Player)
 1311                .WithMany(u => u.OwnedCharacters)
 1312                .HasForeignKey(a => a.PlayerId)
 1313                .OnDelete(DeleteBehavior.Restrict);
 1314
 1315            // Indexes
 1316            entity.HasIndex(a => a.ParentId);
 1317            entity.HasIndex(a => a.WorldId);
 1318            entity.HasIndex(a => a.CampaignId);
 1319            entity.HasIndex(a => a.CreatedBy);
 1320            entity.HasIndex(a => a.Type);
 1321            entity.HasIndex(a => a.Title);
 1322
 1323            // Unique constraint: Slug must be unique among siblings
 1324            // For root articles (ParentId is null), scope by WorldId
 1325            entity.HasIndex(a => new { a.WorldId, a.Slug })
 1326                .IsUnique()
 1327                .HasFilter("[ParentId] IS NULL")
 1328                .HasDatabaseName("IX_Articles_WorldId_Slug_Root");
 1329
 1330            // For child articles (ParentId is not null)
 1331            entity.HasIndex(a => new { a.ParentId, a.Slug })
 1332                .IsUnique()
 1333                .HasFilter("[ParentId] IS NOT NULL")
 1334                .HasDatabaseName("IX_Articles_ParentId_Slug");
 2335        });
 1336    }
 337
 338    private static void ConfigureArticleAlias(ModelBuilder modelBuilder)
 339    {
 1340        modelBuilder.Entity<ArticleAlias>(entity =>
 1341        {
 1342            entity.HasKey(aa => aa.Id);
 1343
 1344            // AliasText is required, max 200 characters
 1345            entity.Property(aa => aa.AliasText)
 1346                .HasMaxLength(200)
 1347                .IsRequired();
 1348
 1349            // AliasType is optional (for future use)
 1350            entity.Property(aa => aa.AliasType)
 1351                .HasMaxLength(50);
 1352
 1353            // ArticleAlias -> Article (CASCADE delete - when article is deleted, remove its aliases)
 1354            entity.HasOne(aa => aa.Article)
 1355                .WithMany(a => a.Aliases)
 1356                .HasForeignKey(aa => aa.ArticleId)
 1357                .OnDelete(DeleteBehavior.Cascade);
 1358
 1359            // Index for looking up aliases by article
 1360            entity.HasIndex(aa => aa.ArticleId);
 1361
 1362            // Index for searching aliases (case-insensitive search will be handled in queries)
 1363            entity.HasIndex(aa => aa.AliasText);
 1364
 1365            // Unique constraint: No duplicate aliases on the same article
 1366            entity.HasIndex(aa => new { aa.ArticleId, aa.AliasText })
 1367                .IsUnique()
 1368                .HasDatabaseName("IX_ArticleAliases_ArticleId_AliasText");
 2369        });
 1370    }
 371
 372    private static void ConfigureArticleLink(ModelBuilder modelBuilder)
 373    {
 1374        modelBuilder.Entity<ArticleLink>(entity =>
 1375        {
 1376            entity.HasKey(al => al.Id);
 1377
 1378            // DisplayText max length
 1379            entity.Property(al => al.DisplayText)
 1380                .HasMaxLength(500);
 1381
 1382            // ArticleLink -> SourceArticle (CASCADE delete - when source is deleted, remove its links)
 1383            entity.HasOne(al => al.SourceArticle)
 1384                .WithMany(a => a.OutgoingLinks)
 1385                .HasForeignKey(al => al.SourceArticleId)
 1386                .OnDelete(DeleteBehavior.Cascade);
 1387
 1388            // ArticleLink -> TargetArticle (NO ACTION - SQL Server limitation with multiple cascade paths)
 1389            // When target article is deleted, links must be cleaned up manually or via triggers
 1390            entity.HasOne(al => al.TargetArticle)
 1391                .WithMany(a => a.IncomingLinks)
 1392                .HasForeignKey(al => al.TargetArticleId)
 1393                .OnDelete(DeleteBehavior.NoAction);
 1394
 1395            // Indexes for query performance
 1396            entity.HasIndex(al => al.SourceArticleId);
 1397            entity.HasIndex(al => al.TargetArticleId);
 1398
 1399            // Unique constraint: Prevent duplicate links at same position
 1400            entity.HasIndex(al => new { al.SourceArticleId, al.TargetArticleId, al.Position })
 1401                .IsUnique();
 2402        });
 1403    }
 404
 405    private static void ConfigureArticleExternalLink(ModelBuilder modelBuilder)
 406    {
 1407        modelBuilder.Entity<ArticleExternalLink>(entity =>
 1408        {
 1409            entity.HasKey(ael => ael.Id);
 1410
 1411            // String field max lengths
 1412            entity.Property(ael => ael.Source)
 1413                .HasMaxLength(50)
 1414                .IsRequired();
 1415
 1416            entity.Property(ael => ael.ExternalId)
 1417                .HasMaxLength(200)
 1418                .IsRequired();
 1419
 1420            entity.Property(ael => ael.DisplayTitle)
 1421                .HasMaxLength(500)
 1422                .IsRequired();
 1423
 1424            // ArticleExternalLink -> Article (CASCADE delete - when article is deleted, remove its external links)
 1425            entity.HasOne(ael => ael.Article)
 1426                .WithMany(a => a.ExternalLinks)
 1427                .HasForeignKey(ael => ael.ArticleId)
 1428                .OnDelete(DeleteBehavior.Cascade);
 1429
 1430            // Index for query performance (lookup all external links for an article)
 1431            entity.HasIndex(ael => ael.ArticleId);
 1432
 1433            // Composite index for uniqueness and query performance
 1434            // An article should not have duplicate references to the same external resource
 1435            entity.HasIndex(ael => new { ael.ArticleId, ael.Source, ael.ExternalId })
 1436                .IsUnique()
 1437                .HasDatabaseName("IX_ArticleExternalLinks_ArticleId_Source_ExternalId");
 2438        });
 1439    }
 440
 441    private static void ConfigureWorldLink(ModelBuilder modelBuilder)
 442    {
 1443        modelBuilder.Entity<WorldLink>(entity =>
 1444        {
 1445            entity.HasKey(wl => wl.Id);
 1446
 1447            entity.Property(wl => wl.Url)
 1448                .HasMaxLength(2048)
 1449                .IsRequired();
 1450
 1451            entity.Property(wl => wl.Title)
 1452                .HasMaxLength(200)
 1453                .IsRequired();
 1454
 1455            entity.Property(wl => wl.Description)
 1456                .HasMaxLength(500);
 1457
 1458            // WorldLink -> World (CASCADE delete - when world is deleted, remove its links)
 1459            entity.HasOne(wl => wl.World)
 1460                .WithMany(w => w.Links)
 1461                .HasForeignKey(wl => wl.WorldId)
 1462                .OnDelete(DeleteBehavior.Cascade);
 1463
 1464            // Index for query performance
 1465            entity.HasIndex(wl => wl.WorldId);
 2466        });
 1467    }
 468
 469    private static void ConfigureWorldDocument(ModelBuilder modelBuilder)
 470    {
 1471        modelBuilder.Entity<WorldDocument>(entity =>
 1472        {
 1473            entity.HasKey(wd => wd.Id);
 1474
 1475            entity.Property(wd => wd.FileName)
 1476                .HasMaxLength(255)
 1477                .IsRequired();
 1478
 1479            entity.Property(wd => wd.Title)
 1480                .HasMaxLength(200)
 1481                .IsRequired();
 1482
 1483            entity.Property(wd => wd.BlobPath)
 1484                .HasMaxLength(1024)
 1485                .IsRequired();
 1486
 1487            entity.Property(wd => wd.ContentType)
 1488                .HasMaxLength(100)
 1489                .IsRequired();
 1490
 1491            entity.Property(wd => wd.Description)
 1492                .HasMaxLength(500);
 1493
 1494            // WorldDocument -> World (CASCADE delete - when world is deleted, remove its documents)
 1495            entity.HasOne(wd => wd.World)
 1496                .WithMany(w => w.Documents)
 1497                .HasForeignKey(wd => wd.WorldId)
 1498                .OnDelete(DeleteBehavior.Cascade);
 1499
 1500            // WorldDocument -> Article (SET NULL - when article is deleted, preserve document but clear reference)
 1501            entity.HasOne(wd => wd.Article)
 1502                .WithMany(a => a.Images)
 1503                .HasForeignKey(wd => wd.ArticleId)
 1504                .OnDelete(DeleteBehavior.SetNull);
 1505
 1506            // WorldDocument -> UploadedBy (User)
 1507            entity.HasOne(wd => wd.UploadedBy)
 1508                .WithMany(u => u.UploadedDocuments)
 1509                .HasForeignKey(wd => wd.UploadedById)
 1510                .OnDelete(DeleteBehavior.Restrict);
 1511
 1512            // Index for query performance
 1513            entity.HasIndex(wd => wd.WorldId);
 1514            entity.HasIndex(wd => wd.UploadedById);
 1515            entity.HasIndex(wd => wd.ArticleId)
 1516                .HasFilter("[ArticleId] IS NOT NULL")
 1517                .HasDatabaseName("IX_WorldDocuments_ArticleId");
 2518        });
 1519    }
 520
 521    private static void ConfigureSummaryTemplate(ModelBuilder modelBuilder)
 522    {
 1523        modelBuilder.Entity<SummaryTemplate>(entity =>
 1524        {
 1525            entity.HasKey(st => st.Id);
 1526
 1527            entity.Property(st => st.Name)
 1528                .HasMaxLength(200)
 1529                .IsRequired();
 1530
 1531            entity.Property(st => st.Description)
 1532                .HasMaxLength(500);
 1533
 1534            entity.Property(st => st.PromptTemplate)
 1535                .IsRequired();
 1536
 1537            // SummaryTemplate -> World (optional, for future world-specific templates)
 1538            entity.HasOne(st => st.World)
 1539                .WithMany()
 1540                .HasForeignKey(st => st.WorldId)
 1541                .OnDelete(DeleteBehavior.Cascade);
 1542
 1543            // SummaryTemplate -> Creator (optional, for future user-created templates)
 1544            entity.HasOne(st => st.Creator)
 1545                .WithMany()
 1546                .HasForeignKey(st => st.CreatedBy)
 1547                .OnDelete(DeleteBehavior.Restrict);
 1548
 1549            // Indexes
 1550            entity.HasIndex(st => st.WorldId);
 1551            entity.HasIndex(st => st.IsSystem);
 2552        });
 553
 554        // Article -> SummaryTemplate relationship
 1555        modelBuilder.Entity<Article>(entity =>
 1556        {
 1557            entity.HasOne(a => a.SummaryTemplate)
 1558                .WithMany()
 1559                .HasForeignKey(a => a.SummaryTemplateId)
 1560                .OnDelete(DeleteBehavior.SetNull);
 2561        });
 562
 563        // Campaign -> SummaryTemplate relationship
 1564        modelBuilder.Entity<Campaign>(entity =>
 1565        {
 1566            entity.HasOne(c => c.SummaryTemplate)
 1567                .WithMany()
 1568                .HasForeignKey(c => c.SummaryTemplateId)
 1569                .OnDelete(DeleteBehavior.SetNull);
 2570        });
 571
 572        // Arc -> SummaryTemplate relationship
 1573        modelBuilder.Entity<Arc>(entity =>
 1574        {
 1575            entity.HasOne(a => a.SummaryTemplate)
 1576                .WithMany()
 1577                .HasForeignKey(a => a.SummaryTemplateId)
 1578                .OnDelete(DeleteBehavior.SetNull);
 2579        });
 1580    }
 581
 582    private static void ConfigureResourceProvider(ModelBuilder modelBuilder)
 583    {
 1584        modelBuilder.Entity<ResourceProvider>(entity =>
 1585        {
 1586            // Primary key is Code (string)
 1587            entity.HasKey(rp => rp.Code);
 1588
 1589            entity.Property(rp => rp.Code)
 1590                .HasMaxLength(20)
 1591                .IsRequired();
 1592
 1593            entity.Property(rp => rp.Name)
 1594                .HasMaxLength(200)
 1595                .IsRequired();
 1596
 1597            entity.Property(rp => rp.Description)
 1598                .HasMaxLength(500)
 1599                .IsRequired();
 1600
 1601            entity.Property(rp => rp.DocumentationLink)
 1602                .HasMaxLength(500)
 1603                .IsRequired();
 1604
 1605            entity.Property(rp => rp.License)
 1606                .HasMaxLength(500)
 1607                .IsRequired();
 1608
 1609            entity.Property(rp => rp.IsActive)
 1610                .HasDefaultValue(true);
 1611
 1612            entity.Property(rp => rp.CreatedAt)
 1613                .IsRequired();
 1614
 1615            // Seed initial providers
 1616            entity.HasData(
 1617                new ResourceProvider
 1618                {
 1619                    Code = "srd",
 1620                    Name = "Open 5e API",
 1621                    Description = "System Reference Document for D&D 5th Edition",
 1622                    DocumentationLink = "https://open5e.com/api-docs",
 1623                    License = "https://open5e.com/legal",
 1624                    IsActive = true,
 1625                    CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
 1626                },
 1627                new ResourceProvider
 1628                {
 1629                    Code = "srd14",
 1630                    Name = "SRD 2014",
 1631                    Description = "System Reference Document 5.1",
 1632                    DocumentationLink = "https://www.dndbeyond.com/srd?srsltid=AfmBOooZgD0uD_hbmyYkHEvFJtDJzktTdIa_J_N2G
 1633                    License = "https://opengamingfoundation.org/ogl.html",
 1634                    IsActive = true,
 1635                    CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
 1636                },
 1637                new ResourceProvider
 1638                {
 1639                    Code = "srd24",
 1640                    Name = "SRD 2024",
 1641                    Description = "System Reference Document 5.2.1",
 1642                    DocumentationLink = "https://www.dndbeyond.com/srd?srsltid=AfmBOooZgD0uD_hbmyYkHEvFJtDJzktTdIa_J_N2G
 1643                    License = "https://creativecommons.org/licenses/by/4.0/",
 1644                    IsActive = true,
 1645                    CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
 1646                },
 1647                new ResourceProvider
 1648                {
 1649                    Code = "ros",
 1650                    Name = "Ruins of Symbaroum",
 1651                    Description = "Ruins of Symbaroum source material",
 1652                    DocumentationLink = "https://freeleaguepublishing.com/games/ruins-of-symbaroum/",
 1653                    License = "https://opengamingfoundation.org/ogl.html",
 1654                    IsActive = true,
 1655                    CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
 1656                }
 1657            );
 2658        });
 1659    }
 660
 661    private static void ConfigureWorldResourceProvider(ModelBuilder modelBuilder)
 662    {
 1663        modelBuilder.Entity<WorldResourceProvider>(entity =>
 1664        {
 1665            // Composite primary key
 1666            entity.HasKey(wrp => new { wrp.WorldId, wrp.ResourceProviderCode });
 1667
 1668            entity.Property(wrp => wrp.IsEnabled)
 1669                .IsRequired();
 1670
 1671            entity.Property(wrp => wrp.ModifiedAt)
 1672                .IsRequired();
 1673
 1674            entity.Property(wrp => wrp.ModifiedByUserId)
 1675                .IsRequired();
 1676
 1677            // WorldResourceProvider -> World (CASCADE delete - when world is deleted, remove provider associations)
 1678            entity.HasOne(wrp => wrp.World)
 1679                .WithMany(w => w.WorldResourceProviders)
 1680                .HasForeignKey(wrp => wrp.WorldId)
 1681                .OnDelete(DeleteBehavior.Cascade);
 1682
 1683            // WorldResourceProvider -> ResourceProvider (RESTRICT - don't allow provider deletion if in use)
 1684            entity.HasOne(wrp => wrp.ResourceProvider)
 1685                .WithMany(rp => rp.WorldResourceProviders)
 1686                .HasForeignKey(wrp => wrp.ResourceProviderCode)
 1687                .OnDelete(DeleteBehavior.Restrict);
 1688
 1689            // Index for querying providers by world
 1690            entity.HasIndex(wrp => wrp.WorldId);
 1691
 1692            // Index for querying worlds by provider
 1693            entity.HasIndex(wrp => wrp.ResourceProviderCode);
 2694        });
 1695    }
 696
 697    private static void ConfigureQuest(ModelBuilder modelBuilder)
 698    {
 1699        modelBuilder.Entity<Quest>(entity =>
 1700        {
 1701            entity.HasKey(q => q.Id);
 1702
 1703            // Title is required, max 300 characters
 1704            entity.Property(q => q.Title)
 1705                .HasMaxLength(300)
 1706                .IsRequired();
 1707
 1708            // Description is HTML from TipTap (nullable)
 1709            entity.Property(q => q.Description);
 1710
 1711            // Status enum
 1712            entity.Property(q => q.Status)
 1713                .IsRequired();
 1714
 1715            // IsGmOnly flag
 1716            entity.Property(q => q.IsGmOnly)
 1717                .HasDefaultValue(false);
 1718
 1719            // SortOrder for display ordering
 1720            entity.Property(q => q.SortOrder)
 1721                .HasDefaultValue(0);
 1722
 1723            // Timestamps
 1724            entity.Property(q => q.CreatedAt)
 1725                .IsRequired();
 1726
 1727            entity.Property(q => q.UpdatedAt)
 1728                .IsRequired();
 1729
 1730            // RowVersion for optimistic concurrency
 1731            entity.Property(q => q.RowVersion)
 1732                .IsRowVersion();
 1733
 1734            // Quest -> Arc (CASCADE delete - when arc is deleted, remove its quests)
 1735            entity.HasOne(q => q.Arc)
 1736                .WithMany()
 1737                .HasForeignKey(q => q.ArcId)
 1738                .OnDelete(DeleteBehavior.Cascade);
 1739
 1740            // Quest -> Creator (User) (RESTRICT - don't allow user deletion if they created quests)
 1741            entity.HasOne(q => q.Creator)
 1742                .WithMany(u => u.CreatedQuests)
 1743                .HasForeignKey(q => q.CreatedBy)
 1744                .OnDelete(DeleteBehavior.Restrict);
 1745
 1746            // Indexes per architecture spec
 1747            entity.HasIndex(q => q.ArcId)
 1748                .HasDatabaseName("IX_Quest_ArcId");
 1749
 1750            entity.HasIndex(q => new { q.ArcId, q.Status })
 1751                .HasDatabaseName("IX_Quest_ArcId_Status");
 1752
 1753            entity.HasIndex(q => new { q.ArcId, q.UpdatedAt })
 1754                .HasDatabaseName("IX_Quest_ArcId_UpdatedAt");
 2755        });
 1756    }
 757
 758    private static void ConfigureQuestUpdate(ModelBuilder modelBuilder)
 759    {
 1760        modelBuilder.Entity<QuestUpdate>(entity =>
 1761        {
 1762            entity.HasKey(qu => qu.Id);
 1763
 1764            // Body is HTML from TipTap (required, non-empty)
 1765            entity.Property(qu => qu.Body)
 1766                .IsRequired();
 1767
 1768            // Timestamp
 1769            entity.Property(qu => qu.CreatedAt)
 1770                .IsRequired();
 1771
 1772            // QuestUpdate -> Quest (CASCADE delete - when quest is deleted, remove its updates)
 1773            entity.HasOne(qu => qu.Quest)
 1774                .WithMany(q => q.Updates)
 1775                .HasForeignKey(qu => qu.QuestId)
 1776                .OnDelete(DeleteBehavior.Cascade);
 1777
 1778            // QuestUpdate -> Session (Article) (SET NULL - when session is deleted, preserve the update but clear the r
 1779            entity.HasOne(qu => qu.Session)
 1780                .WithMany()
 1781                .HasForeignKey(qu => qu.SessionId)
 1782                .OnDelete(DeleteBehavior.SetNull);
 1783
 1784            // QuestUpdate -> Creator (User) (RESTRICT - don't allow user deletion if they created updates)
 1785            entity.HasOne(qu => qu.Creator)
 1786                .WithMany(u => u.CreatedQuestUpdates)
 1787                .HasForeignKey(qu => qu.CreatedBy)
 1788                .OnDelete(DeleteBehavior.Restrict);
 1789
 1790            // Indexes per architecture spec
 1791            entity.HasIndex(qu => new { qu.QuestId, qu.CreatedAt })
 1792                .HasDatabaseName("IX_QuestUpdate_QuestId_CreatedAt");
 1793
 1794            entity.HasIndex(qu => qu.SessionId)
 1795                .HasDatabaseName("IX_QuestUpdate_SessionId");
 2796        });
 1797    }
 798}

Methods/Properties

get_Users()
get_Worlds()
get_WorldMembers()
get_WorldInvitations()
get_Campaigns()
get_Arcs()
get_Articles()
get_ArticleAliases()
get_ArticleLinks()
get_ArticleExternalLinks()
get_WorldLinks()
get_WorldDocuments()
get_SummaryTemplates()
get_ResourceProviders()
get_WorldResourceProviders()
get_Quests()
get_QuestUpdates()
.ctor(Microsoft.EntityFrameworkCore.DbContextOptions`1<Chronicis.Api.Data.ChronicisDbContext>)
OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureUser(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureWorld(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureCampaign(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureWorldMember(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureWorldInvitation(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureArc(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureArticle(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureArticleAlias(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureArticleLink(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureArticleExternalLink(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureWorldLink(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureWorldDocument(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureSummaryTemplate(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureResourceProvider(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureWorldResourceProvider(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureQuest(Microsoft.EntityFrameworkCore.ModelBuilder)
ConfigureQuestUpdate(Microsoft.EntityFrameworkCore.ModelBuilder)