A status column had held strings for six years, with no constraint anywhere and three spellings of shipped in production.
enum OrderStatus: string
{
case Pending = 'pending';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
}
// the backing type is int or string. nothing else.
// every case must have a value, all distinct, all literal —
// no expressions, no constants, no computed values.
The backing type must be int or string and the values must be compile-time literals, which rules out deriving one from a constant. Choosing string over int is almost always right for something that reaches a database: an integer column is smaller and produces rows nobody can read, and the cost of the string is bytes rather than correctness. The values are now a public contract — renaming a case is free and changing its value is a migration.