Feedback from an r/dotnet thread (link): a primitive-obsession library that supports EF Core is incomplete if it stops at the C# boundary. The database is reachable by things that aren't the application — migrations, scripts, a person with a SQL client — and any invariant the C# type enforces should also be enforced by the column it lives in. Otherwise NonEmptyString guarantees nothing on read; it just throws when someone else has already written a blank.
UseStrongTypes() today attaches a ValueConverter per strong-type property and maps intervals to endpoint columns. It emits no CHECK constraints, so the invariants are app-side only.
Opt-in, not automatic
This must be opt-in. Turning it on by default would mean any existing solution that upgrades gets a migration full of new constraints, and that migration fails against production data that predates the strong types. That's a hurdle at exactly the moment someone is trying to adopt the library. Proposed surface:
services.AddDbContext<AppDbContext>(options => options
.UseSqlServer(connectionString)
.UseStrongTypes(strongTypes => strongTypes.UseCheckConstraints()));
Off unless asked for. Existing UseStrongTypes() calls keep their current behaviour and produce no model diff.
Constraints to emit
One per mapped property, covering everything the convention already recognises:
| Type |
Constraint |
NonEmptyString |
column is not blank (mirrors IsNullOrWhiteSpace) |
Email |
not blank, length <= 254 |
MailAddress |
not blank |
Positive<T> |
> 0 |
NonNegative<T> |
>= 0 |
Negative<T> |
< 0 |
NonPositive<T> |
<= 0 |
| interval (two-column) |
Start <= End |
Nullable properties need no special casing — a CHECK whose expression evaluates to NULL is satisfied, so a nullable column and a TPH sibling column both behave correctly without an added IS NULL arm.
Naming: CK_<Table>_<Column>_<Rule>, e.g. CK_Users_LoginCount_Positive.
Implementation sketch
An IModelFinalizingConvention registered by the same plugin, walking properties whose CLR type resolves to a strong type and calling AddCheckConstraint. The SQL is provider-specific, so it branches on IDatabaseProvider the way IntervalJsonColumnTypeConvention already does. SQL Server and PostgreSQL first, since the API integration tests run both.
Open questions
- Blank-check parity.
IsNullOrWhiteSpace covers Unicode whitespace; TRIM/btrim don't, and differ per provider. Exact parity isn't reachable in SQL, so the constraint is an approximation and needs documenting as one.
- JSON-mapped intervals.
HasIntervalJsonConversion stores one column; a JSON-path check is provider-specific and awkward. Suggest leaving those uncovered in the first cut.
- Per-property opt-out for a column that can't take a constraint yet (legacy data being cleaned). Worth adding, or does the global switch suffice?
Also needs updating
- API integration tests asserting a raw SQL insert of a violating value is rejected, on both SQL Server and PostgreSQL.
Skill/references/efcore.md — the opt-in call and the constraint table.
Feedback from an r/dotnet thread (link): a primitive-obsession library that supports EF Core is incomplete if it stops at the C# boundary. The database is reachable by things that aren't the application — migrations, scripts, a person with a SQL client — and any invariant the C# type enforces should also be enforced by the column it lives in. Otherwise
NonEmptyStringguarantees nothing on read; it just throws when someone else has already written a blank.UseStrongTypes()today attaches aValueConverterper strong-type property and maps intervals to endpoint columns. It emits noCHECKconstraints, so the invariants are app-side only.Opt-in, not automatic
This must be opt-in. Turning it on by default would mean any existing solution that upgrades gets a migration full of new constraints, and that migration fails against production data that predates the strong types. That's a hurdle at exactly the moment someone is trying to adopt the library. Proposed surface:
Off unless asked for. Existing
UseStrongTypes()calls keep their current behaviour and produce no model diff.Constraints to emit
One per mapped property, covering everything the convention already recognises:
NonEmptyStringIsNullOrWhiteSpace)EmailMailAddressPositive<T>> 0NonNegative<T>>= 0Negative<T>< 0NonPositive<T><= 0Start <= EndNullable properties need no special casing — a
CHECKwhose expression evaluates to NULL is satisfied, so a nullable column and a TPH sibling column both behave correctly without an addedIS NULLarm.Naming:
CK_<Table>_<Column>_<Rule>, e.g.CK_Users_LoginCount_Positive.Implementation sketch
An
IModelFinalizingConventionregistered by the same plugin, walking properties whose CLR type resolves to a strong type and callingAddCheckConstraint. The SQL is provider-specific, so it branches onIDatabaseProviderthe wayIntervalJsonColumnTypeConventionalready does. SQL Server and PostgreSQL first, since the API integration tests run both.Open questions
IsNullOrWhiteSpacecovers Unicode whitespace;TRIM/btrimdon't, and differ per provider. Exact parity isn't reachable in SQL, so the constraint is an approximation and needs documenting as one.HasIntervalJsonConversionstores one column; a JSON-path check is provider-specific and awkward. Suggest leaving those uncovered in the first cut.Also needs updating
Skill/references/efcore.md— the opt-in call and the constraint table.