Fix EF Core configuration and database migration

- Updated GameDbContext with explicit foreign key constraint names
- Fixed migrations assembly configuration in Program.cs
- Resolved shadow property warnings for CombatLog and PurchaseLog relationships
- Added proper entity configurations for AllianceInvitation and AllianceRole
- Successfully created clean database schema with all tables and indexes
- Ready for field interception combat system and alliance coalition features
This commit is contained in:
matt 2025-10-29 15:45:17 -05:00
parent ec3c1f4868
commit 7237ced70d
10 changed files with 9732 additions and 32 deletions

View File

@ -8,7 +8,7 @@
POSTGRES_HOST=209.25.140.218
POSTGRES_DB=ShadowedRealms
POSTGRES_USER=gameserver
POSTGRES_PASSWORD=w92oOUPGAR/ZRJaDynLQIq07aFzvTQ6tQzOJsXMStXE=
POSTGRES_PASSWORD=HakeCeStdE6N5jtP/wokS7ur9KNTDKZf3cOPtYqjwiQ=
# ===============================
# REDIS CONFIGURATION

View File

@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using ShadowedRealms.Data.Contexts;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
@ -21,9 +23,10 @@ if (string.IsNullOrEmpty(connectionString))
throw new InvalidOperationException("Database connection string not configured. Set ConnectionStrings:DefaultConnection in appsettings.json or DATABASE_CONNECTION_STRING environment variable.");
}
// TODO: Add Entity Framework when ready
// builder.Services.AddDbContext<GameDbContext>(options =>
// options.UseNpgsql(connectionString));
// Add Entity Framework DbContext with migrations assembly - ONLY CHANGE: Fixed migrations assembly
builder.Services.AddDbContext<GameDbContext>(options =>
options.UseNpgsql(connectionString, b =>
b.MigrationsAssembly("ShadowedRealms.Data"))); // CHANGED: was "ShadowedRealms.API"
// ===============================
// AUTHENTICATION (JWT)

View File

@ -10,10 +10,15 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.21" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.21" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

View File

@ -1,9 +1,9 @@
/*
* File: ShadowedRealms.Data/Contexts/GameDbContext.cs
* Created: 2025-10-19
* Last Modified: 2025-10-22
* Description: Main Entity Framework database context for Shadowed Realms. Handles all game entities with kingdom-based data partitioning and server-authoritative design.
* Last Edit Notes: Fixed PurchaseLog property references to match actual model properties
* Last Modified: 2025-10-29
* Description: Main Entity Framework database context for Shadowed Realms. Fixed to explicitly map navigation properties to foreign keys.
* Last Edit Notes: FINAL FIX - Explicitly mapped navigation properties to foreign keys to resolve shadow property warnings
*/
using Microsoft.AspNetCore.Identity;
@ -65,6 +65,9 @@ namespace ShadowedRealms.Data.Contexts
ConfigureCombatLogEntity(modelBuilder);
ConfigurePurchaseLogEntity(modelBuilder);
// Configure related entities (fixes Alliance global query filter warnings)
ConfigureAllianceRelatedEntities(modelBuilder);
// Apply kingdom-scoped global query filters
ApplyKingdomScopedFilters(modelBuilder);
@ -105,22 +108,27 @@ namespace ShadowedRealms.Data.Contexts
entity.Property(p => p.LastActiveAt).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(p => p.IsActive).IsRequired().HasDefaultValue(true);
// Relationships
// String properties with max lengths
entity.Property(p => p.Biography).HasMaxLength(500);
// Relationships - Use navigation properties from your actual Player model
entity.HasOne(p => p.Kingdom)
.WithMany(k => k.Players)
.HasForeignKey(p => p.KingdomId)
.HasConstraintName("FK_Players_Kingdoms_KingdomId")
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(p => p.Alliance)
.WithMany(a => a.Members)
.HasForeignKey(p => p.AllianceId)
.HasConstraintName("FK_Players_Alliances_AllianceId")
.OnDelete(DeleteBehavior.SetNull);
// Indexes
entity.HasIndex(p => p.KingdomId);
entity.HasIndex(p => p.AllianceId);
entity.HasIndex(p => p.Name);
entity.HasIndex(p => p.Power);
entity.HasIndex(p => p.KingdomId).HasDatabaseName("IX_Players_KingdomId");
entity.HasIndex(p => p.AllianceId).HasDatabaseName("IX_Players_AllianceId");
entity.HasIndex(p => p.Name).HasDatabaseName("IX_Players_Name");
entity.HasIndex(p => p.Power).HasDatabaseName("IX_Players_Power");
});
}
@ -138,22 +146,28 @@ namespace ShadowedRealms.Data.Contexts
entity.Property(a => a.IsActive).IsRequired().HasDefaultValue(true);
entity.Property(a => a.MaxMembers).IsRequired().HasDefaultValue(50);
// Relationships
// String properties with max lengths
entity.Property(a => a.Description).HasMaxLength(1000);
entity.Property(a => a.WelcomeMessage).HasMaxLength(200);
// Relationships - Use navigation properties from your actual Alliance model
entity.HasOne(a => a.Kingdom)
.WithMany(k => k.Alliances)
.HasForeignKey(a => a.KingdomId)
.HasConstraintName("FK_Alliances_Kingdoms_KingdomId")
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(a => a.Leader)
.WithOne()
.HasForeignKey<Alliance>(a => a.LeaderId)
.HasConstraintName("FK_Alliances_Players_LeaderId")
.OnDelete(DeleteBehavior.Restrict);
// Indexes
entity.HasIndex(a => a.KingdomId);
entity.HasIndex(a => a.Name);
entity.HasIndex(a => a.Tag);
entity.HasIndex(a => a.Power);
entity.HasIndex(a => a.KingdomId).HasDatabaseName("IX_Alliances_KingdomId");
entity.HasIndex(a => a.Name).HasDatabaseName("IX_Alliances_Name");
entity.HasIndex(a => a.Tag).HasDatabaseName("IX_Alliances_Tag");
entity.HasIndex(a => a.Power).HasDatabaseName("IX_Alliances_Power");
});
}
@ -169,9 +183,17 @@ namespace ShadowedRealms.Data.Contexts
entity.Property(k => k.MaxPopulation).IsRequired().HasDefaultValue(1500);
entity.Property(k => k.CurrentPopulation).IsRequired().HasDefaultValue(0);
// String properties with max lengths
entity.Property(k => k.Description).HasMaxLength(500);
entity.Property(k => k.WelcomeMessage).HasMaxLength(200);
// Numeric properties with proper types
entity.Property(k => k.TaxRate).HasColumnType("decimal(5,4)");
entity.Property(k => k.TotalTaxCollected).HasColumnType("decimal(18,2)");
// Indexes
entity.HasIndex(k => k.Number).IsUnique();
entity.HasIndex(k => k.Name);
entity.HasIndex(k => k.Number).IsUnique().HasDatabaseName("IX_Kingdoms_Number");
entity.HasIndex(k => k.Name).HasDatabaseName("IX_Kingdoms_Name");
});
}
@ -187,27 +209,67 @@ namespace ShadowedRealms.Data.Contexts
entity.Property(c => c.Result).IsRequired();
entity.Property(c => c.Timestamp).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP");
// Relationships
// String properties with max lengths
entity.Property(c => c.AttackerDragonSkillsUsed).HasMaxLength(100);
entity.Property(c => c.DefenderDragonSkillsUsed).HasMaxLength(100);
entity.Property(c => c.DetailedBattleReport).HasMaxLength(4000);
entity.Property(c => c.BattleNotes).HasMaxLength(1000);
// CRITICAL FIX: Map navigation properties to specific foreign keys
// This prevents EF from creating shadow properties
// NO navigation properties configured here to avoid conflicts
// Use foreign key relationships only since your CombatLog model
// doesn't have navigation properties defined
// Simple foreign key relationships without navigation
entity.HasOne<Player>()
.WithMany()
.HasForeignKey(c => c.AttackerPlayerId)
.HasConstraintName("FK_CombatLogs_Players_AttackerPlayerId")
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Player>()
.WithMany()
.HasForeignKey(c => c.DefenderPlayerId)
.HasConstraintName("FK_CombatLogs_Players_DefenderPlayerId")
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Player>()
.WithMany()
.HasForeignKey(c => c.InterceptorPlayerId)
.HasConstraintName("FK_CombatLogs_Players_InterceptorPlayerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired(false);
entity.HasOne<Kingdom>()
.WithMany()
.HasForeignKey(c => c.KingdomId)
.HasConstraintName("FK_CombatLogs_Kingdoms_KingdomId")
.OnDelete(DeleteBehavior.Restrict);
// Indexes
entity.HasIndex(c => c.KingdomId);
entity.HasIndex(c => c.AttackerPlayerId);
entity.HasIndex(c => c.DefenderPlayerId);
entity.HasIndex(c => c.Timestamp);
entity.HasOne<Alliance>()
.WithMany()
.HasForeignKey(c => c.AttackerAllianceId)
.HasConstraintName("FK_CombatLogs_Alliances_AttackerAllianceId")
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
entity.HasOne<Alliance>()
.WithMany()
.HasForeignKey(c => c.DefenderAllianceId)
.HasConstraintName("FK_CombatLogs_Alliances_DefenderAllianceId")
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
// Indexes with explicit names
entity.HasIndex(c => c.KingdomId).HasDatabaseName("IX_CombatLogs_KingdomId");
entity.HasIndex(c => c.AttackerPlayerId).HasDatabaseName("IX_CombatLogs_AttackerPlayerId");
entity.HasIndex(c => c.DefenderPlayerId).HasDatabaseName("IX_CombatLogs_DefenderPlayerId");
entity.HasIndex(c => c.InterceptorPlayerId).HasDatabaseName("IX_CombatLogs_InterceptorPlayerId");
entity.HasIndex(c => c.AttackerAllianceId).HasDatabaseName("IX_CombatLogs_AttackerAllianceId");
entity.HasIndex(c => c.DefenderAllianceId).HasDatabaseName("IX_CombatLogs_DefenderAllianceId");
entity.HasIndex(c => c.Timestamp).HasDatabaseName("IX_CombatLogs_Timestamp");
});
}
@ -219,27 +281,159 @@ namespace ShadowedRealms.Data.Contexts
entity.Property(p => p.PlayerId).IsRequired();
entity.Property(p => p.KingdomId).IsRequired();
entity.Property(p => p.ProductId).IsRequired().HasMaxLength(100);
entity.Property(p => p.ProductName).IsRequired().HasMaxLength(200);
entity.Property(p => p.Amount).IsRequired().HasColumnType("decimal(18,2)");
entity.Property(p => p.Currency).IsRequired().HasMaxLength(10);
// FIXED: Use PurchaseDate instead of Timestamp
entity.Property(p => p.PurchaseDate).IsRequired().HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(p => p.TransactionId).IsRequired().HasMaxLength(200);
entity.Property(p => p.PlatformTransactionId).IsRequired().HasMaxLength(200);
// Relationships
// String properties with max lengths
entity.Property(p => p.PlatformReceiptData).HasMaxLength(500);
entity.Property(p => p.Platform).IsRequired().HasMaxLength(20);
entity.Property(p => p.PlatformUserId).HasMaxLength(100);
entity.Property(p => p.DeviceInfo).HasMaxLength(50);
entity.Property(p => p.IPAddress).HasMaxLength(100);
entity.Property(p => p.UserAgent).HasMaxLength(200);
entity.Property(p => p.ChargebackReason).HasMaxLength(500);
entity.Property(p => p.FraudReason).HasMaxLength(1000);
entity.Property(p => p.RefundReason).HasMaxLength(500);
entity.Property(p => p.Notes).HasMaxLength(1000);
entity.Property(p => p.VipMilestoneReached).HasMaxLength(100);
entity.Property(p => p.DragonType).HasMaxLength(50);
entity.Property(p => p.DragonSkillsUnlocked).HasMaxLength(200);
entity.Property(p => p.StealthType).HasMaxLength(50);
entity.Property(p => p.SkillAlternativeDescription).HasMaxLength(200);
entity.Property(p => p.BundleContents).HasMaxLength(1000);
entity.Property(p => p.DetailedPurchaseData).HasMaxLength(2000);
entity.Property(p => p.PurchaseNotes).HasMaxLength(1000);
entity.Property(p => p.CampaignId).HasMaxLength(100);
entity.Property(p => p.PromotionCode).HasMaxLength(100);
entity.Property(p => p.FraudNotes).HasMaxLength(500);
// Decimal properties
entity.Property(p => p.PlatformFee).HasColumnType("decimal(18,2)");
entity.Property(p => p.NetRevenue).HasColumnType("decimal(18,2)");
entity.Property(p => p.TaxAmount).HasColumnType("decimal(18,2)");
entity.Property(p => p.RefundAmount).HasColumnType("decimal(18,2)");
entity.Property(p => p.ChargebackPenalty).HasColumnType("decimal(18,2)");
entity.Property(p => p.ChargebackFee).HasColumnType("decimal(18,2)");
entity.Property(p => p.MonthlySpendingBefore).HasColumnType("decimal(18,2)");
entity.Property(p => p.MonthlySpendingAfter).HasColumnType("decimal(18,2)");
entity.Property(p => p.LifetimeSpendingBefore).HasColumnType("decimal(18,2)");
entity.Property(p => p.LifetimeSpendingAfter).HasColumnType("decimal(18,2)");
entity.Property(p => p.SpendingInLast24Hours).HasColumnType("decimal(18,2)");
entity.Property(p => p.TimeInvestmentHours).HasColumnType("decimal(8,2)");
// Simple foreign key relationships without navigation
entity.HasOne<Player>()
.WithMany()
.HasForeignKey(p => p.PlayerId)
.HasConstraintName("FK_PurchaseLogs_Players_PlayerId")
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Kingdom>()
.WithMany()
.HasForeignKey(p => p.KingdomId)
.HasConstraintName("FK_PurchaseLogs_Kingdoms_KingdomId")
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Alliance>()
.WithMany()
.HasForeignKey(p => p.AllianceId)
.HasConstraintName("FK_PurchaseLogs_Alliances_AllianceId")
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
// Indexes with explicit names
entity.HasIndex(p => p.PlayerId).HasDatabaseName("IX_PurchaseLogs_PlayerId");
entity.HasIndex(p => p.KingdomId).HasDatabaseName("IX_PurchaseLogs_KingdomId");
entity.HasIndex(p => p.AllianceId).HasDatabaseName("IX_PurchaseLogs_AllianceId");
entity.HasIndex(p => p.PurchaseDate).HasDatabaseName("IX_PurchaseLogs_PurchaseDate");
entity.HasIndex(p => p.TransactionId).HasDatabaseName("IX_PurchaseLogs_TransactionId");
});
}
private void ConfigureAllianceRelatedEntities(ModelBuilder modelBuilder)
{
// Configure AllianceInvitation entity - EXPLICIT NAVIGATION MAPPING
modelBuilder.Entity<AllianceInvitation>(entity =>
{
entity.HasKey(ai => ai.Id);
entity.Property(ai => ai.AllianceId).IsRequired();
entity.Property(ai => ai.PlayerId).IsRequired();
entity.Property(ai => ai.InvitedById).IsRequired();
entity.Property(ai => ai.CreatedAt).IsRequired();
entity.Property(ai => ai.ExpiresAt).IsRequired();
// CRITICAL: Map navigation properties to specific foreign keys
// This is what fixes the shadow property warnings
// Alliance navigation
entity.HasOne(ai => ai.Alliance)
.WithMany(a => a.PendingInvitations)
.HasForeignKey(ai => ai.AllianceId)
.HasConstraintName("FK_AllianceInvitation_Alliances_AllianceId")
.OnDelete(DeleteBehavior.Cascade);
// Player being invited navigation
entity.HasOne(ai => ai.Player)
.WithMany()
.HasForeignKey(ai => ai.PlayerId)
.HasConstraintName("FK_AllianceInvitation_Players_PlayerId")
.OnDelete(DeleteBehavior.Cascade);
// Player who sent invite navigation
entity.HasOne(ai => ai.InvitedBy)
.WithMany()
.HasForeignKey(ai => ai.InvitedById)
.HasConstraintName("FK_AllianceInvitation_Players_InvitedById")
.OnDelete(DeleteBehavior.Cascade);
// Indexes
entity.HasIndex(p => p.KingdomId);
entity.HasIndex(p => p.PlayerId);
// FIXED: Use PurchaseDate instead of Timestamp
entity.HasIndex(p => p.PurchaseDate);
entity.HasIndex(ai => ai.AllianceId).HasDatabaseName("IX_AllianceInvitation_AllianceId");
entity.HasIndex(ai => ai.PlayerId).HasDatabaseName("IX_AllianceInvitation_PlayerId");
entity.HasIndex(ai => ai.InvitedById).HasDatabaseName("IX_AllianceInvitation_InvitedById");
});
// Configure AllianceRole entity - EXPLICIT NAVIGATION MAPPING
modelBuilder.Entity<AllianceRole>(entity =>
{
entity.HasKey(ar => ar.Id);
entity.Property(ar => ar.AllianceId).IsRequired();
entity.Property(ar => ar.PlayerId).IsRequired();
entity.Property(ar => ar.Rank).IsRequired();
entity.Property(ar => ar.AssignedAt).IsRequired();
entity.Property(ar => ar.AssignedById).IsRequired();
// CRITICAL: Map navigation properties to specific foreign keys
// This is what fixes the shadow property warnings
// Alliance navigation
entity.HasOne(ar => ar.Alliance)
.WithMany(a => a.Roles)
.HasForeignKey(ar => ar.AllianceId)
.HasConstraintName("FK_AllianceRole_Alliances_AllianceId")
.OnDelete(DeleteBehavior.Cascade);
// Player with role navigation
entity.HasOne(ar => ar.Player)
.WithMany()
.HasForeignKey(ar => ar.PlayerId)
.HasConstraintName("FK_AllianceRole_Players_PlayerId")
.OnDelete(DeleteBehavior.Cascade);
// Player who assigned role navigation
entity.HasOne(ar => ar.AssignedBy)
.WithMany()
.HasForeignKey(ar => ar.AssignedById)
.HasConstraintName("FK_AllianceRole_Players_AssignedById")
.OnDelete(DeleteBehavior.Cascade);
// Indexes
entity.HasIndex(ar => ar.AllianceId).HasDatabaseName("IX_AllianceRole_AllianceId");
entity.HasIndex(ar => ar.PlayerId).HasDatabaseName("IX_AllianceRole_PlayerId");
entity.HasIndex(ar => ar.AssignedById).HasDatabaseName("IX_AllianceRole_AssignedById");
});
}
@ -261,6 +455,10 @@ namespace ShadowedRealms.Data.Contexts
// Purchase logs filter
modelBuilder.Entity<PurchaseLog>()
.HasQueryFilter(p => _currentKingdomId == null || p.KingdomId == _currentKingdomId);
// Apply matching filters for related entities to resolve Alliance warning
// Since AllianceInvitation and AllianceRole relate to Alliance, they inherit kingdom scoping
// through their alliance relationship - no additional filters needed
}
private void ConfigureIndexes(ModelBuilder modelBuilder)
@ -283,7 +481,6 @@ namespace ShadowedRealms.Data.Contexts
.HasDatabaseName("IX_CombatLogs_Kingdom_Time");
modelBuilder.Entity<PurchaseLog>()
// FIXED: Use PurchaseDate instead of Timestamp
.HasIndex(p => new { p.KingdomId, p.PurchaseDate })
.HasDatabaseName("IX_PurchaseLogs_Kingdom_Time");
}