Files
AdamuSw/src/Persistence/EntityFramework/EntityDataContext.cs
nolt 9e0bcede2f Cascade delete mini game ranking entries
A character which has participated in a mini game with enabled ranking
statistics could not be deleted. The database rejected it with

  Npgsql.PostgresException 23503: update or delete on table "Character"
  violates foreign key constraint "FK_MiniGameRankingEntry_Character_CharacterId"

MiniGameRankingEntry is a member of neither the character nor the mini game
definition aggregate, so the model generator doesn't emit a delete behavior
for its references. The entity was registered without any additional
configuration, so Entity Framework fell back to no action for these optional
relationships, which the database translates into a restricting foreign key.

Both references are now configured with a cascading delete, matching the
behavior of the other references to a character. With that, all seven
relationships pointing to Character delete their dependents.

This affects both deletion paths: the one of the player
(DeleteCharacterAction) and the one of an administrator, who can remove a
character from the account in the admin panel.

The cascade on the mini game definition is deliberate and part of the same
defect: deleting a definition in the admin panel ran into the very same
restriction. It does mean that the ranking history of a mini game is dropped
together with its definition.

Reported in issue 796.

(cherry picked from commit a391bdedbbd3a36ccb37124125bd7ede4f851dca)
2026-07-27 00:53:27 +03:00

101 lines
4.3 KiB
C#

// <copyright file="EntityDataContext.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.EntityFramework;
using Microsoft.EntityFrameworkCore;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.Persistence.EntityFramework.Extensions;
using MUnique.OpenMU.Persistence.EntityFramework.Extensions.ModelBuilder;
using MUnique.OpenMU.Persistence.EntityFramework.Model;
/// <summary>
/// Context for all types of the data model.
/// </summary>
public class EntityDataContext : ExtendedTypeContext
{
/// <summary>
/// Gets or sets the current game configuration.
/// This is used by the <see cref="ConfigurationTypeRepository{T}"/> which gets its data from the current game configuration.
/// </summary>
internal GameConfiguration? CurrentGameConfiguration { get; set; }
/// <inheritdoc/>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!ConnectionConfigurator.IsInitialized)
{
ConnectionConfigurator.Initialize(new ConfigFileDatabaseConnectionStringProvider());
}
base.OnConfiguring(optionsBuilder);
this.Configure(optionsBuilder);
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<AppearanceData>(o => o.Ignore(p => p.CharacterStatus)); // todo
modelBuilder.Ignore<ConstantElement>();
modelBuilder.Ignore<SimpleElement>();
modelBuilder.Entity<Model.AttributeDefinition>();
modelBuilder.Entity<ConnectServerDefinition>();
modelBuilder.Entity<ChatServerDefinition>();
modelBuilder.Entity<MiniGameRankingEntry>().Apply();
modelBuilder.Entity<GameServerDefinition>(entity =>
{
entity.Property(e => e.PvpEnabled).HasDefaultValue(true);
});
modelBuilder.Entity<ConfigurationUpdate>().Apply();
modelBuilder.Entity<ConfigurationUpdateState>();
modelBuilder.Entity<SystemConfiguration>();
modelBuilder.Entity<PowerUpDefinitionValue>().Apply();
modelBuilder.Entity<Model.ConstValueAttribute>().Apply();
modelBuilder.Entity<Account>().Apply();
modelBuilder.Entity<Character>().Apply();
modelBuilder.Entity<CharacterClass>().Apply();
modelBuilder.Entity<DropItemGroup>().Apply();
modelBuilder.Entity<ExitGate>().Apply();
modelBuilder.Entity<GameConfiguration>().Apply();
modelBuilder.Entity<GameMapDefinition>().Apply();
modelBuilder.Entity<ItemCrafting>().Apply();
modelBuilder.Entity<ItemDefinition>().Apply();
modelBuilder.Entity<ItemLevelBonusTable>().Apply();
modelBuilder.Entity<ItemDropItemGroup>().Apply();
modelBuilder.Entity<ItemOptionCombinationBonus>().Apply();
modelBuilder.Entity<ItemOptionDefinition>().Apply();
modelBuilder.Entity<ItemOptionType>().Apply();
modelBuilder.Entity<ItemSetGroup>().Apply();
modelBuilder.Entity<ItemSlotType>().Apply();
modelBuilder.Entity<ItemStorage>().Apply();
modelBuilder.Entity<ItemBasePowerUpDefinition>().Apply();
modelBuilder.Entity<LevelBonus>().Apply();
modelBuilder.Entity<MagicEffectDefinition>().Apply();
modelBuilder.Entity<MasterSkillRoot>().Apply();
modelBuilder.Entity<MiniGameChangeEvent>().Apply();
modelBuilder.Entity<MiniGameDefinition>().Apply();
modelBuilder.Entity<MiniGameSpawnWave>().Apply();
modelBuilder.Entity<MonsterDefinition>().Apply();
modelBuilder.Entity<MonsterSpawnArea>().Apply();
modelBuilder.Entity<Skill>().Apply();
modelBuilder.Entity<SkillComboDefinition>().Apply();
modelBuilder.Entity<SkillEntry>().Apply();
modelBuilder.Entity<MasterSkillDefinition>().Apply();
modelBuilder.Entity<LetterBody>().Apply();
modelBuilder.Entity<LetterHeader>().Apply();
modelBuilder.Entity<QuestDefinition>().Apply();
modelBuilder.Entity<WarpInfo>().Apply();
// join entity keys:
this.AddJoinDefinitions(modelBuilder);
modelBuilder.UseGuidV7Ids();
GuildContext.ConfigureModel(modelBuilder);
FriendContext.ConfigureModel(modelBuilder);
}
}