Table of Contents

Snowflake Conversion

Discord IDs are 64-bit ulong snowflakes. Most relational providers — including PostgreSQL/Npgsql and SQL Server — lack native unsigned 64-bit support and store integers as signed long. Persistord bridges the gap in one place so you never have to annotate individual properties.

How it works

Persistord.Core ships two EF Core value converters:

  • UlongToLongConverter — converts ulong ↔ long using unchecked((long)value) and unchecked((ulong)value).
  • NullableUlongToLongConverter — the same round-trip for ulong? ↔ long?.

The unchecked cast is bit-faithful: the 64 bits are reinterpreted without range checking, so every value — including snowflakes with the high bit set — survives storage exactly.

Global registration

DiscordDbContext registers both converters in ConfigureConventions, so the conversion applies to every ulong and ulong? property in your derived context automatically:

protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
    builder.Properties<ulong>().HaveConversion<UlongToLongConverter>();
    builder.Properties<ulong?>().HaveConversion<NullableUlongToLongConverter>();
}

You never annotate individual IDs. Inherit DiscordDbContext and all snowflake properties in your model — including those in module entities — are handled.

Keys

The conversion above covers values: it makes a ulong storable at all. It says nothing about whether EF should generate that value or expect the caller to supply it. A separate SnowflakeKeyConvention, also registered by DiscordDbContext, handles that half: it marks every ulong or ulong? property that is part of a primary key ValueGeneratedNever(), so EF never treats a snowflake key as a store-generated identity column. This runs for the skeleton entities and for the consumer's own entities alike — inherit DiscordDbContext (or DiscordGraphDbContext) and any ulong key you add gets the same treatment without a fluent call. Explicit configuration still wins: the convention writes at convention precedence, so a property you've configured yourself (with ValueGeneratedOnAdd(), for example) is left as you configured it.

This is a breaking change from 1.0.0-beta2, where a bare ulong primary key was store-generated by EF's own default convention. See Upgrading from 1.0.0-beta2 for what changes in your next migration and how to opt back into a store-generated key if you genuinely want one.

Storage note

Snowflakes are stored as long in the database column. Discord snowflakes remain below long.MaxValue until roughly the year 2084, so signed storage is safe in practice. The converter is nevertheless bit-faithful and would round-trip correctly even past that point. Both the conversion and the key convention operate on "all unsigned 64-bit", not specifically "Discord snowflakes" — a Steam64 id or any other ulong property in your model goes through them the same way, and that's intended.

See also

  • Core Graph — the conventions-only DiscordDbContext, the opt-in DiscordGraphDbContext, and the skeleton entities whose ulong IDs this conversion covers.