Repository navigation
SchemaKit: a public plugin contract, so packages contribute to the model differ and migrations SQL generator instead of replacing them #56
Description
Problem
EF Core resolves IMigrationsModelDiffer and IMigrationsSqlGenerator as single services. A package that needs either one replaces it (ReplaceService, as this package does today). When two packages do, one silently wins (the last ReplaceService call), and the other package's declarations vanish from migrations without an error.
So this package can't be one of several migration-aware packages. That is the reason for the new name (#37): SchemaKit is the one owner of both services in the CodoMetis family, and everything else that needs them plugs in. "Schema" is the boundary. Whatever needs the differ or the migrations SQL generator belongs here or plugs in. Types, operators and query translation stay out.
Change
ROADMAP.md D10 (section 5.3) planned contributors as several DI registrations through an options extension, as an internal seam. Make it the public contract from 11.0.0:
-
A contributor interface (diff a model pair into operations, render an operation per provider) and a registration that adds contributors without touching the two EF services.
-
The built-in features (complex and expression indexes, exclusion constraints, typed filters, …) are contributors themselves, so the package's own features prove the contract, not only a sample.
-
Dependency direction: an ecosystem package ships an optional satellite that references the SchemaKit kernel, the way
CodoMetis.ValueRanges.NodaTimereferences its core. SchemaKit references none of them. The first prospective consumer is the ltree support in the planned CodoMetis.Hierarchies package: trigger-maintained paths, path checks, and any index options Npgsql'sHasMethod/HasOperatorscan't express. Its type and query translation never need the contract. -
The contract is public API under the same compatibility promise as the declaration API. The wiring call is renamed along with it (
UseSchemaKit(),ROADMAP.md5.6). -
A migration renders the same SQL whichever package version applies it. A migration stores operations, not SQL. The generator renders them when the migration is applied or scripted, so an operation rendered by whatever contributor version is restored today means something different from what was scaffolded. That matters most for an operation that installs a body: a trigger function (the Hierarchies paths above), a check, a SQL function. The contract fixes the rule:
- every operation carries the contract version it was diffed under, and a contributor keeps a renderer for every version it has shipped, or
- the operation carries its rendered SQL from scaffolding time, frozen in the migration file.
Changing a body is a new version. The snapshot records the version, so upgrading the package produces a migration that moves the database to the new body, instead of an old migration quietly applying it. Objects whose meaning a running application depends on (a function a query calls) use versioned names, so instances on the old and the new version can run side by side during a rolling deployment.
Done when
- A contributor written in a test project outside the package adds an operation that
migrations add,GenerateCreateScriptand the pending-changes check all see, on PostgreSQL and on SQL Server. - A migration scaffolded with one version of a test contributor, applied and scripted with the next version (whose body changed), renders the first version's SQL. The upgrade itself shows up as a pending model change.
- The built-in features are contributors.
docs/has a page on writing a contributor.- It ships in 11.0.0, because the contract can't change in a minor afterwards.
Activity
CaffeinatedCoder commentedon Oct 26, 2026
Added a requirement: a migration renders the same SQL whichever package version applies it, through versioned rendering or SQL frozen at scaffolding time, plus the matching Done-when line. It came up while evaluating SQL functions that ValueRanges could install through a contributor (CaffeinatedCoder/CodoMetis.ValueRanges#479, option 2). It applies to every contributor that installs a body, Hierarchies' trigger functions first.