Troubleshooting
Stored IDs look negative
Cause: Discord snowflakes are 64-bit ulong values. Many relational providers
lack native unsigned 64-bit support and store integers as signed long. Snowflakes
with the high bit set appear negative when cast to long.
Fix: This is expected. Persistord uses a bit-faithful unchecked((long)value)
cast so the 64 bits are reinterpreted without range checking — the value round-trips
exactly. No data is lost. See Snowflake Conversion.
Soft-deleted messages are missing from queries
Cause: ApplyMessagesModule() installs a global EF Core query filter that hides
rows where IsDeleted = true. This filter is on by default.
Fix: Use IgnoreQueryFilters() for a single query, or disable the filter globally
at startup with ApplyMessagesModule(filterDeleted: false). See
Soft-delete & Query Filters.
// Per-query
var all = db.Messages.IgnoreQueryFilters().ToList();
// Global — disable at startup
modelBuilder.ApplyMessagesModule(filterDeleted: false);
dotnet ef can't find the DbContext
Cause: dotnet ef looks for a DbContext in the startup project. Persistord
ships no migrations and no executable startup project.
Fix: Pass --project pointing at your bot project, which owns the derived
context:
dotnet ef migrations add Initial --project YourBot.csproj
dotnet ef database update --project YourBot.csproj
See Migrations.
Owned embed collections create extra tables
Cause: EF Core synthesizes surrogate shadow keys for owned collection types stored
relationally. Embed, EmbedField, AttachmentEntity, and ReactionEntity each
get their own table with a generated key. This is expected behaviour.
Fix: If you prefer document storage over relational child tables, use e.ToJson()
on a provider with strong JSON support (PostgreSQL, SQL Server):
modelBuilder.Entity<MessageEntity>().OwnsMany(m => m.Embeds, e =>
{
e.ToJson(); // power-user opt-in — not the default
});
ToJson() is not the default because owned-collection JSON support varies by
provider. See Messages.
My unique index accepts duplicates
Cause: A nullable column is part of the natural key the index covers. SQL
unique indexes treat NULL as distinct from every other NULL, so two rows
that both have NULL in that column don't count as a conflict — the index
never rejects the second insert.
Fix: Use a non-nullable sentinel value instead of null for "no value here".
ManagedResource.Scope is a non-nullable string, "" for "global", for
exactly this reason. See Providers
and Scope.
ORDER BY on a DateTimeOffset fails on SQLite
Cause: EF Core's SQLite provider cannot translate an OrderBy/OrderByDescending
over a DateTimeOffset column — it throws System.NotSupportedException: SQLite does not support expressions of type 'DateTimeOffset' in ORDER BY clauses.
Fix: Order by a surrogate key instead. Every ManagedResource carries a
long Id for this reason:
var recent = await db.Set<ManagedMessage>().OrderByDescending(m => m.Id).ToListAsync();
See Providers.
CryptographicException when reading a token
Cause: Persistord.Protection decrypts a [Protected] column while EF
materializes the row. A read only fails this way when the specific Data
Protection key that encrypted the value is gone from the key ring — deleted, or
explicitly revoked.
Fix: Restoring a deleted key from a backup of the key ring folder fixes a
read broken by deletion. A revoked key has no supported recovery through
Persistord.Protection — treat revocation as permanent, and back up the key
ring on the same schedule as the database itself. See
Protection.
History foreign-key violation when deleting a message
Cause: MessageHistoryEntity holds a real foreign key to MessageEntity
configured with DeleteBehavior.Restrict. Hard-deleting the parent MessageEntity
row violates this constraint and is blocked by the database.
Fix: Never hard-delete messages. Set IsDeleted = true and DeletedAt instead
(soft-delete). The row survives physically, the FK stays valid, and the history —
including the row logging the deletion — remains intact. See
Soft-delete & Query Filters.