Entity Configuration — Namespace Note
All entity configuration classes in this directory use the namespace
FishMMO.Database.Npgsql.Entities even though they reside under
Npgsql/EntityConfigurations/ in the project tree.
This divergence is intentional: EF Core discovers IEntityTypeConfiguration<T>
implementations by type scanning. NpgsqlDbContext.OnModelCreating calls
modelBuilder.ApplyConfigurationsFromAssembly(typeof(NpgsqlDbContext).Assembly),
which relies on the declaring assembly -- not the namespace or folder path. Keeping the
same namespace as the entity models avoids using aliases and keeps configuration
files consistent with the entities they configure.
If a future reader finds this surprising, please keep this note in mind rather than reorganising files to match the folder structure.
What is here
55 files, one per table, mirroring the folders under Npgsql/Entities/. The table name a
configuration declares with ToTable(...) is the authority for the schema; the C# names are
<Name>Entity / <Name>EntityConfiguration.
| Folder | Tables |
|---|---|
| (root) | deployment_secrets |
Login/ |
accounts, auth_tokens, connection_token_keys, email_queue, login_servers, login_server_signing_keys, two_factor_recovery_codes |
World/ |
world_servers, kick_requests |
Scene/ |
scenes, scene_servers, chat, quests, group_finder_queue |
Scene/Character/ |
characters plus 22 per-character tables, including character_item, character_waypoints and currency_ledger |
Scene/Guild/ |
guilds, guild_rank, guild_log, guild_application, guild_updates |
Scene/Party/ |
parties, party_updates |
Scene/Arena/ |
arena_match, arena_match_member, arena_season, arena_rating, arena_penalty |
Scene/Housing/ |
plots, plot_structures, plot_access, plot_vault, plot_updates |
The full per-table inventory lives in ../../README.md.
Conventions these files follow
- Timestamps default with
HasDefaultValueSql("timezone('UTC', CURRENT_TIMESTAMP)"), never a client clock, so rows written by different scene servers order correctly under clock skew. - Every index a service's hot query depends on is declared here, with a comment naming the query
it serves. Services write their upserts as raw SQL with an
ON CONFLICT (columns)list, so a unique index declared here is load-bearing for that statement: change its columns and the upsert starts failing at runtime, not at compile time. - Partial indexes are used wherever the interesting rows are a minority of the table
(
HasFilter("active = TRUE"),HasFilter("tax_due_utc IS NOT NULL"),HasFilter("owner_character_id <> 0"), and similar). - Enum-valued columns are stored as
intwith a comment fixing the numeric meaning, because this assembly cannot referenceFishMMO.Shared. The pairing is pinned by tests (ItemContainerTypeParityTests,CurrencyLedgerStateTests), not by a shared type.
Configurations added in this window
Scene/Character/CharacterItemEntityConfiguration.csreplacedCharacterInventoryEntityConfiguration,CharacterEquipmentEntityConfigurationandCharacterBankEntityConfiguration, all three deleted. Onecharacter_itemtable keyed by the item's own id;(character_id, container, slot)is a unique index, not the upsert conflict target, and is checked per row — see the file's comment for why a straight slot swap would trip it and howCharacterItemService.SaveSnapshotAsyncavoids emitting one.Scene/Character/CharacterWaypointEntityConfiguration.cs— composite key(CharacterID, SceneName, Page), which is both the upsert's arbiter and the prefix scan the character load uses.Maskis a bitmask page merged withmask | EXCLUDED.maskinCharacterWaypointService; the table is deliberately not versioned last-writer-wins, since a character never un-discovers a place.SceneNameis capped at 64 characters.Scene/Character/CurrencyLedgerEntityConfiguration.cs— append-only history.Amountis always positive; direction comes fromState(Absorbed= 1,Returned= 2, andCurrencyLedgerService.RecordAsyncrejects anything else, including theUnsettleddefault). Two composite indexes:ix_currency_ledger_character_timefor per-character history andix_currency_ledger_reason_timefor per-sink aggregation over a window.Scene/GroupFinderQueueEntityConfiguration.cs— the shared dungeon and arena queue.CharacterIDis uniquely indexed (a character cannot be placed in two groups at once);SceneTypedefaults to2(SceneType.Group) because every row predating arenas was a dungeon row; the matcher's covering index is(WorldServerID, SceneType, SceneName, Difficulty, Status, TimeCreated), andLastPulseis indexed for the stale-row reaper. Rows cascade with the character.Scene/Arena/— five tables.arena_matchhas a unique index onInstanceID(the hosting scene server resolves the match from the instance it loaded);arena_match_memberis unique on(MatchID, CharacterID);arena_seasonuses a partial unique index (HasFilter("active = TRUE")) so at most one season is active;arena_ratingis unique on(SeasonID, CharacterID)with a(SeasonID, Rating)leaderboard index and defaults of1500rating /1500peak;arena_penaltyis the deserter lock, one row per character.Scene/Housing/— five tables.plotsis unique on(WorldServerID, SceneName, PlotKey)— plots are world-scoped, andPlotKeyis capped at 64 to matchPlotIdentity.MaxPlotKeyLength— with a filtered unique indexix_plots_one_house_per_characterenforcing one house per character and a filteredTaxDueUtcindex for the tax sweep.plot_structures,plot_accessandplot_updatescascade from the plot;plot_vaultdeliberately has no foreign key toplots(it holds what a character is owed after the plot is gone) and cascades from the character instead.