Doka.EntityFrameworkCore.SafeMigrations.MySql
10.4.1
dotnet add package Doka.EntityFrameworkCore.SafeMigrations.MySql --version 10.4.1
NuGet\Install-Package Doka.EntityFrameworkCore.SafeMigrations.MySql -Version 10.4.1
<PackageReference Include="Doka.EntityFrameworkCore.SafeMigrations.MySql" Version="10.4.1" />
<PackageVersion Include="Doka.EntityFrameworkCore.SafeMigrations.MySql" Version="10.4.1" />
<PackageReference Include="Doka.EntityFrameworkCore.SafeMigrations.MySql" />
paket add Doka.EntityFrameworkCore.SafeMigrations.MySql --version 10.4.1
#r "nuget: Doka.EntityFrameworkCore.SafeMigrations.MySql, 10.4.1"
#:package Doka.EntityFrameworkCore.SafeMigrations.MySql@10.4.1
#addin nuget:?package=Doka.EntityFrameworkCore.SafeMigrations.MySql&version=10.4.1
#tool nuget:?package=Doka.EntityFrameworkCore.SafeMigrations.MySql&version=10.4.1
Doka.EntityFrameworkCore.SafeMigrations
SafeMigrations is a fail-closed EF Core 10 migration library for databases whose starting schema may differ between application instances. It supports one canonical migration sequence across MySQL, MariaDB, and PostgreSQL without assuming a common legacy migration history or deleting unknown objects.
The library classifies each operation against the live catalog as missing,
matching, transition_ready, different, unsupported, data_blocked, or
prerequisite_missing. It then applies one provider-neutral policy. An
operation either converges safely, remains an idempotent no-op, or stops with a
stable reason. It never guesses that two unknown objects are semantically
equivalent.
Platform and packages
- .NET 10 and EF Core 10
Doka.EntityFrameworkCore.SafeMigrations: provider-neutral intent, definitions, planning, reports, andMigrationBuilderextensionsDoka.EntityFrameworkCore.SafeMigrations.MySql: MySQL and MariaDB adapter on the publicDoka.EntityFrameworkCore.MySql10.4.0 operation-handler and typed migration-metadata SPIDoka.EntityFrameworkCore.SafeMigrations.PostgreSql: PostgreSQL adapter on Npgsql 10
The declared release-qualification matrix is:
| Provider package | Engines |
|---|---|
.MySql |
MySQL 8.4 and 9.7; MariaDB 10.11, 11.4, 11.8, and 12.3 |
.PostgreSql |
PostgreSQL 14 through 18, with one release-gate cell per supported major |
The CI and release workflows pin the exact patch tags and image digests used when that matrix executes. The exact successful run, not this table, is release evidence. See Support and qualification.
The initial complete stable delivery is 10.0.0. The latest confirmed published release is 10.4.0. This source is prepared for stable 10.4.1, which preserves accepted prerequisites across ordered brownfield convergence, binds semantic aliases to physical catalog identities, invalidates stale projected evidence, and revalidates MySQL/MariaDB physical keys before replacement. Only a successful release run and verified public packages establish 10.4.1 availability or qualification. See the changelog.
Installation
Install one provider package. The core package is included transitively. The commands select the prepared stable release exactly so restore does not move to a different package version implicitly. Confirm the matching published release and all three NuGet package pages before installation; source or changelog entries alone do not establish package availability.
package_version='10.4.1'
dotnet package add Doka.EntityFrameworkCore.SafeMigrations.MySql --version "$package_version"
or:
package_version='10.4.1'
dotnet package add Doka.EntityFrameworkCore.SafeMigrations.PostgreSql --version "$package_version"
The .NET package command documents exact-version selection. Verify release identity and content using Release verification.
Provider registration
MySQL and MariaDB use the same SafeMigrations adapter. Doka's server-version profile determines the active engine capabilities.
using Doka.EntityFrameworkCore.MySql;
using Doka.EntityFrameworkCore.SafeMigrations;
using Doka.EntityFrameworkCore.SafeMigrations.MySql;
services.AddDbContext<AppDbContext>(options =>
{
options.UseMySql(connectionString, serverVersion);
options.UseMySqlSafeMigrations();
});
UseMySqlSafeMigrations() declares its user-variable requirement through Doka
10.4.0. For a provider-owned connection string, Doka supplies
AllowUserVariables=true when it was omitted. An explicitly contradictory
setting is rejected. Caller-owned DbConnection and MySqlDataSource inputs
are never mutated and must already use AllowUserVariables=true and
GuidFormat=Binary16. Doka also requires matched-row semantics
(UseAffectedRows=false) on every connection path. SafeMigrations retains a
runtime validation immediately before guarded commands as defense in depth;
errors do not disclose the connection string.
PostgreSQL registration is additive to UseNpgsql:
using Doka.EntityFrameworkCore.SafeMigrations;
using Doka.EntityFrameworkCore.SafeMigrations.PostgreSql;
services.AddDbContext<AppDbContext>(options =>
{
options.UseNpgsql(connectionString);
options.UsePostgreSqlSafeMigrations();
});
Applications that deliberately own EF Core's internal service provider must also register the matching provider services:
services.AddEntityFrameworkDokaMySql();
services.AddEntityFrameworkDokaMySqlSafeMigrations();
or:
services.AddEntityFrameworkNpgsql();
services.AddPostgreSqlSafeMigrations();
Missing or conflicting SafeMigrations integration fails before target DDL and before the migration history row is written.
Registration replaces EF Core's scoped IMigrationsAssembly so migrations,
the model snapshot, scripts, bundles, and IMigrator all use the same
canonical context. The non-generic overload keeps EF's exact runtime-context
behavior. A derived instance context must name its canonical base explicitly:
options.UseMySqlSafeMigrations<ApplicationDbContext>();
options.UsePostgreSqlSafeMigrations<ApplicationDbContext>();
The canonical type must be assignable from the runtime context. PostgreSQL applications with a custom migrations generator must compose it explicitly:
options.UsePostgreSqlSafeMigrations<CustomNpgsqlMigrationsSqlGenerator, ApplicationDbContext>();
Automatic safe migration scaffolding
SafeMigrations integrates with EF Core's design-time service pipeline. With the
normal direct Microsoft.EntityFrameworkCore.Design reference used by
dotnet ef, or a direct Microsoft.EntityFrameworkCore.Tools reference used
by Visual Studio's Package Manager Console, migration scaffolding automatically
writes safe table and index calls. The Tools package supplies EF Design
transitively; SafeMigrations recognizes both official package layouts. Do not
copy a generated CreateTable body into ExpectedTableDefinition by hand.
Every generated migration explicitly imports
Doka.EntityFrameworkCore.SafeMigrations; no application global using is
required for its extension methods or policy types.
A runtime-only project may reference a SafeMigrations provider without either design-time package. It builds without a design-service attribute or warning; EF's tooling rejects scaffolding until a supported design-time package is added.
A generator that references the SafeMigrations provider source project directly
does not receive NuGet buildTransitive assets. Register the provider-owned
design-time services explicitly in that generator's Properties/AssemblyInfo.cs:
using Microsoft.EntityFrameworkCore.Design;
[assembly: DesignTimeServicesReference(
"Doka.EntityFrameworkCore.SafeMigrations.MySql.MySqlSafeMigrationDesignTimeServices, "
+ "Doka.EntityFrameworkCore.SafeMigrations.MySql",
"Doka.EntityFrameworkCore.MySql")]
This source-development registration is not required for an ordinary package consumer. Do not implement a second migrations code generator.
EF Core reads referenced design-time services from both the migration target
assembly and the startup assembly. Keep the provider package's buildTransitive
assets enabled on every path that supplies it to those projects. SafeMigrations
also places an internal guard at the start of every non-empty model difference.
Its generator consumes that guard before provider code generation. If the
design-time assets are missing, excluded, or stale, EF Core's ordinary generator
rejects SafeMigrationDesignTimeServicesRequiredOperation before it can write a
normal CreateTable migration. Remove any partial output, restore, rebuild the
target and startup projects, and rerun the command; do not accept the ordinary
migration. SafeMigrations consumes the same marker without generating SQL or
changing the database when EF Core reuses the decorated runtime model differ
for migration-history DDL.
Configure the scaffolding mode
SafeMigrationScaffoldingMode is the design-time switch used by both provider
registrations:
| Value | Selection | Generated table behavior | Generated rollback |
|---|---|---|---|
Strict |
Default; use for normal migrations | CreateTableIfNotExists requires an existing table to match the complete generated definition |
DropTableIfExists |
LegacyConvergence |
Select only while scaffolding a reviewed legacy baseline | ConvergeTableFromModel adds missing table children; its source-frozen policy rejects drift by default or repairs the documented safe allowlist |
Entire Down body throws before DDL |
The no-argument registration selects Strict:
options.UseMySqlSafeMigrations();
// or: options.UsePostgreSqlSafeMigrations();
This is equivalent to the explicit MySQL/MariaDB configuration:
using Doka.EntityFrameworkCore.SafeMigrations;
options.UseMySqlSafeMigrations(safeMigrations =>
{
safeMigrations.UseScaffoldingMode(SafeMigrationScaffoldingMode.Strict);
});
PostgreSQL uses the same options contract:
using Doka.EntityFrameworkCore.SafeMigrations;
options.UsePostgreSqlSafeMigrations(safeMigrations =>
{
safeMigrations.UseScaffoldingMode(SafeMigrationScaffoldingMode.Strict);
});
For each migration that deliberately adopts heterogeneous legacy
installations, select the mode and policy before scaffolding. Omit
UseLegacyConvergencePolicy to retain the fail-closed
ThrowIfDifferent default:
options.UseMySqlSafeMigrations(safeMigrations =>
{
safeMigrations
.UseScaffoldingMode(
SafeMigrationScaffoldingMode.LegacyConvergence)
.UseLegacyConvergencePolicy(
SafeMigrationPolicy.RepairIfSafe);
});
or:
options.UsePostgreSqlSafeMigrations(safeMigrations =>
{
safeMigrations
.UseScaffoldingMode(
SafeMigrationScaffoldingMode.LegacyConvergence)
.UseLegacyConvergencePolicy(
SafeMigrationPolicy.RepairIfSafe);
});
Then create and review the migration normally:
dotnet ef migrations add CoreLegacyConvergence
Before selecting a mode, review the complete
migration authoring guide. It shows the actual
generated CreateTableIfNotExists and ConvergeTableFromModel source as well
as the supported hand-authored ExpectedTableDefinition plus ConvergeTable
form, including their different rollback behavior.
The generated Up method uses ConvergeTableFromModel plus safe index helpers
and writes the selected policy as an explicit named argument. Its Down method
throws before DDL because SafeMigrations cannot know which objects predated that
migration. After the legacy baseline sequence has been scaffolded, return
registration to the no-argument strict default for newly created tables. The
selected behavior is frozen in each generated C# migration; changing either
option never reinterprets an existing migration.
The configure callback is available on the canonical-context overloads too:
options.UseMySqlSafeMigrations<ApplicationDbContext>(safeMigrations =>
{
safeMigrations
.UseScaffoldingMode(
SafeMigrationScaffoldingMode.LegacyConvergence)
.UseLegacyConvergencePolicy(SafeMigrationPolicy.RepairIfSafe);
});
options.UsePostgreSqlSafeMigrations<ApplicationDbContext>(safeMigrations =>
{
safeMigrations
.UseScaffoldingMode(
SafeMigrationScaffoldingMode.LegacyConvergence)
.UseLegacyConvergencePolicy(SafeMigrationPolicy.RepairIfSafe);
});
An application that composes a custom PostgreSQL baseline generator can select the mode on that overload as well:
options.UsePostgreSqlSafeMigrations<CustomNpgsqlMigrationsSqlGenerator, ApplicationDbContext>(
safeMigrations =>
{
safeMigrations
.UseScaffoldingMode(
SafeMigrationScaffoldingMode.LegacyConvergence)
.UseLegacyConvergencePolicy(SafeMigrationPolicy.RepairIfSafe);
});
UseScaffoldingMode and UseLegacyConvergencePolicy configure generated source
only. They do not change how an already generated migration executes and are
not runtime switches for existing migration files. The policy accepts only
ThrowIfDifferent and RepairIfSafe; ExistenceOnly, undefined enum values,
and a non-default legacy policy without LegacyConvergence fail during options
configuration.
Automatic rewriting covers scaffolded CreateTable, CreateIndex,
DropIndex, and DropTable operations plus data changes derived from
HasData. The structural calls become CreateTableIfNotExists, a safe
index-create helper, DropIndexIfExists, and DropTableIfExists. Model-managed
inserts, updates, and deletes become source-frozen SafeMigrations operations.
Other EF operations remain ordinary EF migration operations. When a later
migration needs catalog-aware idempotent handling for a column, constraint,
rename, or schema operation, use the corresponding SafeMigrations builder API
and review the resulting contract. This boundary prevents the design-time
layer from silently assigning policies to operations whose repair or ownership
semantics require an explicit choice.
Provider identity annotations on scaffolded columns are captured immutably and
participate in fingerprints, live-catalog comparison, and final DDL. This
preserves MySQL/MariaDB AUTO_INCREMENT and PostgreSQL identity semantics.
Doka's ClientGuid strategy is retained for replay and hashing but compared as
non-AUTO_INCREMENT catalog state because it generates values in the client,
not in the database. HiLo, storage-format, unknown column, and unsupported
operation-level annotations remain Unsupported before target DDL instead of
being ignored.
For MySQL and MariaDB, the scaffolder also projects Doka's typed index-prefix
metadata into explicit *WithPrefixesIfNotExistsFromModel calls. A zero entry
means the complete key; a positive entry limits that key to the declared number
of characters or bytes according to the engine contract. No provider annotation
is left on the outer SafeMigrations operation. The complete generated source is
shown in the migration authoring guide.
The *FromModel helpers are public because generated migrations must compile
against a stable package API. They are scaffolder targets, not required
hand-written boilerplate. For a manually authored index contract, use
CreateIndexIfNotExists or EnsureIndex instead.
Model-managed data from HasData
Keep model-managed rows in the normal EF model:
modelBuilder.Entity<Role>().HasData(
new Role
{
Id = 1,
Name = "Administrator",
});
With either SafeMigrations scaffolding mode enabled, the next ordinary
dotnet ef migrations add command replaces EF's generated model-managed
InsertData, UpdateData, and DeleteData calls with
EnsureModelManagedDataFromModel, UpdateModelManagedDataFromModel, and
DeleteModelManagedDataFromModel. No second definition and no manual migration
rewrite are required.
For an initial migration, preflight projects an accepted table creation before its generated model-managed ensure. The empty-schema deployment is therefore ready without treating an existing or otherwise unknown table as empty.
An ensure inserts only an absent primary-key row and treats an equal row as a
no-op. An update or delete proceeds only while the complete source-frozen row
still matches; otherwise it rejects rather than overwriting external changes.
Delete also rejects unmodeled dependent-row effects. Every mutation verifies
its target postcondition, and a second successful execution is a no-op.
Captured values retain their CLR type and store type through provider
conversion, so converter-backed Guid formats, enums, and char(1) values use
the same relational mapping contract during preflight and guarded execution.
The generated source contains the values by design. Model snapshots and SQL
scripts already contain the same model-managed data. Do not place secrets,
environment-specific values, mutable operational data, or large datasets in
HasData; use EF Core UseSeeding/UseAsyncSeeding or an application-owned
bootstrap workflow for those cases. Existing migration files are immutable:
remove and re-scaffold an unapplied raw-data migration after upgrading, but
correct an already applied migration only through a new forward migration.
See Model-managed data authoring for the generated ensure, update, and delete source and conflict behavior.
Independent application and extended migration ownership
A derived ExtendedApplicationDbContext can retain shared application entities
for runtime queries and relationships while ApplicationDbContext owns their
tables. Mark those tables as excluded from migrations, then opt in once on the
extended context:
options.UseMySqlSafeMigrations(safeMigrations =>
{
safeMigrations.ExcludeModelManagedDataForExcludedTables();
});
The PostgreSQL registration exposes the same option. It suppresses only newly calculated EF model-managed insert, update, and delete differences for exact relational tables that the relevant source or target model excludes from migrations. It does not remove entities from the runtime model, reinterpret an existing migration, or infer ownership from inheritance or naming. The default remains fail-closed. Design-time and runtime configuration must agree.
See Model-managed-data ownership for the independent context, migration project, snapshot, assembly, history-table, and EF CLI contract.
Policies
Preflight is not a migration policy. It is a separate read-only runner outside
IMigrator and EF migration history.
| Policy | Existing matching object | Existing different object |
|---|---|---|
ExistenceOnly |
No-op | No-op only where existence semantics are explicit |
ThrowIfDifferent |
No-op | Reject |
RepairIfSafe |
No-op | Apply a proven allowlisted repair, otherwise reject |
ExistenceOnly is intended for a table container in a granular convergence
baseline. It is not a complete table-definition check.
Heterogeneous legacy convergence
The automatically scaffolded ConvergeTableFromModel call solves the case
where one instance has no table, another has an empty copied table, and a third
has only some columns or constraints. It snapshots EF's typed table definition
and emits one existence-only table-container operation followed by granular
operations for every owned column and constraint. Scaffolded indexes follow as
their own safe operations. Those children use the policy written into the
generated call; the default is ThrowIfDifferent. With explicit
RepairIfSafe, an ordinary existing column may also use a provider-proven,
lossless VARCHAR widening or data-verified narrowing. MySQL and MariaDB may
additionally repair the exact Boolean transition BIT(1) -> TINYINT(1).
PostgreSQL independently qualifies character varying widening and narrowing;
the Boolean transition does not apply there. Narrowing performs a grouped live
character-length scan and repeats its proof at execution. One overlength value,
an incomplete proof, or concurrent violating data stops without truncation.
SafeMigrations separately reports that accepted column DDL may rewrite a table;
data safety is not an online-DDL promise.
All other pre-existing requirements remain: the resolved character family,
collation, generated/identity state, row-version state, provider metadata, and
dependent indexes or constraints must be fully understood and compatible. A
MySQL/MariaDB VARCHAR column on either side of a foreign key remains blocked
because the required coupled type transition is outside a single-column repair.
PostgreSQL preserves compatible ordinary foreign keys across its independently
qualified length change. Doka's exact Boolean conversion accepts only absent,
null, false, or true literal defaults. An expression default or a foreign-key
dependency keeps the Boolean transition blocked because a single-column repair
cannot prove that separate behavioral or coupled-type contract.
Doka 10.4.0's typed metadata must recognize every MySQL/MariaDB annotation.
Unknown, malformed, contradictory, or unsupported metadata rejects. Existing
NULL rows make a NOT NULL repair DataBlocked. Other type-family,
collation, computed/generated, identity, row-version, and unsupported
provider-metadata drift rejects without mutation. The table container alone
never hides missing children.
ExpectedTableDefinition and ConvergeTable remain available for advanced
hand-authored contracts, for example when a reviewed migration needs a policy
or expected definition that cannot be inferred from the current EF model. They
are no longer required boilerplate for the normal first convergence migration.
The migration authoring guide compares both
convergence forms with the generated strict default.
For an existing table, missing nullable or default-bearing columns can be
added. Unsafe NOT NULL additions, conflicting definitions, duplicate unique
values, orphaned foreign keys, and violated checks stop before their target
DDL. Unknown extra objects are reported and preserved.
The convergence baseline should be forward-only. Its Down method must reject
automatic reconstruction of an unknown legacy origin; recovery uses a tested
backup/restore path or a forward fix.
Read-only preflight and postflight
Resolve ISafeMigrationRunner from the configured context. Use a pseudonymous
instance ID, never a host name, database name, credential, or connection
string.
using Doka.EntityFrameworkCore.SafeMigrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
var runner = context.GetService<ISafeMigrationRunner>();
var options = new SafeMigrationRunOptions(
instanceId: "tenant-7f3d8b1c",
targetMigrationId: "202608170001_CoreConvergence");
var preflight = await runner.AnalyzePendingMigrationsAsync(context, options, CancellationToken.None);
if (preflight.Status != SafeMigrationReportStatus.Ready)
{
throw new InvalidOperationException("Deployment requires a reviewed, safe-only ready preflight.");
}
var targetMigration = preflight.TargetMigrationId
?? throw new InvalidOperationException("Preflight did not identify a migration target.");
await context.GetService<IMigrator>().MigrateAsync(targetMigration, CancellationToken.None);
This narrow example executes only a safe-only Ready report and binds execution
to the exact analyzed target, not the latest migration in the assembly.
ReadyWithProviderOperations requires separate review and postconditions for
ordinary provider operations; NoOperations requires checking intended history
and postconditions rather than executing an unqualified target. A blocked report
must stop deployment. Propagate a deployment cancellation token when available.
Typed EF seed/update/delete-data operations retain preceding structural facts
for a later non-unique safe index, but they remain independently reviewable and
invalidate every projected or live pre-batch data-safety proof. A later unique
index or additive data-validating constraint therefore remains fail-closed;
subsequent structural provider operations do not clear that uncertainty.
Keep the migration assembly fixed and the required write/DDL fences in place;
preflight does not reserve database state. The
deployment runbook owns these checks
and postflight. EF's targeted migrator
uses a null target to mean latest, so the example rejects a missing target.
For an explicit execution contract, use AnalyzeAsync before migration. Use
VerifyAsync afterwards with the reviewed final-state contract. When the same
exact safe schema, table, column, index, primary-key, or named-constraint
resource is written more than once, postflight treats only its final safe
writer as authoritative. Earlier assessments remain ordered and report
postcondition_superseded with a satisfied effective postcondition. Ordinary
provider operations never supersede a safe postcondition because their effects
are not owned or inferred. A rename proves source absence only; add an explicit
ensure for the destination when its complete final definition must be verified.
The postflight procedure
binds each contract's fingerprint to the same deployment artifact and target.
Reports include provider and engine identity, model and operation-contract
SHA-256 fingerprints, ordered assessments, preserved unexpected objects, and
stable codes. Report schema version 2 separates provider AnalysisCode from
the policy DecisionCode, carries bounded typed facet differences, and states
the known OperationalImpact. Detailed evidence remains in the report, never
in metric labels, and model-managed values remain redacted. A blocked preflight
can be raised as SafeMigrationPreflightException through
report.ThrowIfBlocked() while retaining the complete immutable report.
The contract fingerprint covers safe intents, definitions, policies, operation
annotations, and order; ordinary provider operations contribute only their CLR
type, not their SQL or other properties. Retain the immutable artifact digest
and independent review for those operations. Serialize with
SafeMigrationReportJson; the package includes the current
safe-migration-run-report-v2 schema.
The version 1 schema remains
available for readers of previously persisted reports.
For focused operator output, serialize a self-describing report view instead of copying or mutating the immutable report:
var blockingJson = SafeMigrationReportJson.SerializeToUtf8Bytes(
preflight,
SafeMigrationReportSelection.BlockingOnly);
Complete includes every entry, NonMatching removes only fully converged safe
assessments, and BlockingOnly includes only the assessments that block the
current preflight or postflight phase. The view retains source identity and
total/included counts, preserves assessment order, and never includes
unexpected objects in BlockingOnly. It uses the distinct packaged
safe-migration-report-view-v1 schema;
the existing one-argument serializer remains the canonical complete report v2.
Do not encode a preflight-only operation inside Migration.Up. EF would record
the migration as applied after successful command execution even when the
target DDL was intentionally omitted.
Supported operations
The sealed SafeMigrationOperation envelope covers all of these families:
- ensure and drop schema
- ensure, drop, and rename table
- ensure, drop, rename, and alter column
- ensure, drop, and rename index
- ensure and drop primary key
- ensure and drop unique constraint
- ensure and drop check constraint
- ensure and drop foreign key
Expected definitions snapshot all input collections and model relevant facets.
Defaults distinguish no default, literal null, typed literals, and SQL
expressions. Provider catalog queries are parameterized; identifiers and DDL
are rendered through provider SQL services.
SQL-bearing definitions should use the typed SafeMigrationSql expression
tree. Typed identifiers, literals, operators, null tests, ranges, lists,
functions, casts, collations, and current date/time values can be rendered for
DDL and compared structurally against provider catalog output. For example:
var nonNegative = ExpectedCheckConstraintDefinition.FromExpression(
"ck_orders_total_non_negative",
"orders",
SafeMigrationSql.Binary(
SafeMigrationSql.Identifier("total"),
SafeMigrationSqlBinaryOperator.GreaterThanOrEqual,
SafeMigrationSql.Literal(0)));
On MySQL and MariaDB, a typed literal or Cast store type is mapped to the
bounded CAST grammar shared by both engines. Integer column aliases such as
int and bigint unsigned render as SIGNED and UNSIGNED; string column
aliases render through CHAR. A type without an exact common mapping is
classified as structured_cast_type before target DDL instead of being copied
into SQL. A typed null retains its requested type on every provider.
PostgreSQL validates the requested store type through Npgsql's relational type
mapping, then SafeMigrations renders documented built-in aliases in their
catalog-canonical form in CAST or ::<type> syntax. This prevents drift
between aliases such as int4 and integer and uses the same fail-closed
classification for unknown type grammar. PostgreSQL float and float(p) are
normalized according to their documented binary-precision ranges before type
mapping, including array forms.
MariaDB generated columns cannot preserve a NOT NULL facet; that
unrepresentable definition fails before DDL with
generated_column_nullability, while MySQL retains and verifies the facet.
EF-scaffolded check constraints using this bounded grammar are converted to
the same structured tree automatically. Unsupported SQL stops scaffolding
before a migration file is accepted; use an explicit FromExpression
definition for a reviewed equivalent. The complete authoring behavior and
failure boundary are documented in
Migration authoring paths.
Legacy raw SQL remains representable as opaque input, but opaque expressions
cannot authorize Matching; they are classified with
opaque_sql_expression. After an identifier rename, an affected opaque facet
is classified with opaque_expression_rename_projection. This is deliberate:
neither provider guesses semantic equivalence from SQL text.
MySQL and MariaDB do not provide PostgreSQL-style schema namespaces, so schema
operations are classified as unsupported there. Provider-specific features
such as PostgreSQL filtered, included, operator-class, collation, descending,
and null-distinctness index facets are explicit rather than silently degraded.
An omitted column collation means the exact provider-inferred effective
default, never an ignored comparison facet. Index key direction and null order
distinguish provider default from explicit ASC, DESC, NULLS FIRST, and
NULLS LAST.
Collation identity is structured rather than dot-split text:
var collation = new SafeMigrationCollationIdentifier(
name: "tenant.collation",
schema: "collation_catalog");
PostgreSQL resolves the exact schema/name identity to its catalog OID. MySQL
and MariaDB accept unqualified collation names; a schema-qualified identity is
classified as schema_qualified_collation before target DDL because those
engines do not expose PostgreSQL-style collation namespaces.
Multiple DbContext instances
All application instances may use a runtime class derived from one canonical
ApplicationDbContext, but its effective relational model must equal the
canonical migration snapshot. SafeMigrations checks that equality before
preflight when the configured migrations assembly supplies a snapshot. Without
a snapshot,
the runner still fingerprints the runtime model but cannot compare it to a
canonical snapshot. Supply an independently established
expectedModelFingerprint when using a snapshot-free explicit contract; a
fingerprint computed from the same unchecked instance is not a target-model
proof. Keep the canonical snapshot in normal EF migration deployments.
Instance-specific schema extensions require a separate DbContext, migration
assembly, and history table. A different target model per instance cannot share
one deterministic application migration sequence.
Ensure operations use semantic database-object identity. If the requested name
is absent but a differently named primary key, unique constraint, check
constraint, foreign key, or index has the same complete modeled definition, the
operation is Matching and executes as a no-op. If the requested name exists,
its definition is authoritative: any facet drift is Different even when a
second alias matches. A differently named object with a different definition
does not suppress normal safe creation. Multiple equivalent aliases remain
non-destructive no-ops. Drop and rename operations always target the exact
physical name; SafeMigrations never drops or renames an alias to enforce a
naming convention. A non-equivalent singleton or an occupied physical
namespace required by provider DDL is Different, not Missing; the guard
rejects before the server can raise a raw duplicate-object error.
Operational contract
- Run one migrator per database. Provider migration locks serialize competing replicas against the same database; different databases may migrate in parallel.
- Keep out-of-band DDL disabled during preflight and migration.
- Establish a write fence or maintenance window for data-sensitive constraints and backfills.
- MySQL and MariaDB DDL can commit implicitly. Their guard uses session-local
temporary state and prepared DDL, not stored routines, and requires no
CREATE ROUTINEprivilege. - PostgreSQL guarded operations participate in the normal EF migration transaction. A baseline generator that emits a transaction-suppressed command for a guarded operation is rejected before that operation executes. Ordinary provider operations and externally executed no-transaction scripts have separate boundaries described in the deployment runbook.
- PostgreSQL analysis owns one read-only
RepeatableReadtransaction and transaction-scoped advisory lock. If the caller already owns a transaction, it must be read-only and useRepeatableReadorSerializable; otherwise analysis fails before reading the catalog and leaves that transaction owned by the caller. - Always run postflight and retain its report with deployment evidence.
Analyzer work is deterministically bounded at 32 operations per statement that
the optimizer sees and eight statements per ADO.NET transport batch. MySQL and
MariaDB capture provider plans in 512-operation windows while retaining the
complete expected unique-index catalog. Every transport batch remains bounded
by 16,000 parameters and 4 MiB of UTF-8 payload; MySQL/MariaDB additionally cap
a batch at half the live max_allowed_packet.
Configured EF command timeouts apply to catalog commands and batches. A single
operation that exceeds a bound is rejected before query execution; a failed
later batch never publishes a partial report. The live provider suites qualify
100,000 deterministically ordered mixed operations on every supported server
profile. The workload covers every observed state and planned action across
tables, columns, indexes, and primary-key, unique, check, and foreign-key
constraints. Native ADO.NET batching is used only when
DbConnection.CanCreateBatch is true. Compatible connection wrappers without
batch support execute the same bounded statements sequentially and retain
timeout, cancellation, ordinal, and report-atomicity guarantees.
See Deployment and recovery and Failure codes.
Build and qualification
The SDK is fixed by global.json.
dotnet restore Doka.EntityFrameworkCore.SafeMigrations.slnx --locked-mode
dotnet build Doka.EntityFrameworkCore.SafeMigrations.slnx --configuration Release --no-restore
dotnet test tests/Doka.EntityFrameworkCore.SafeMigrations.Tests/Doka.EntityFrameworkCore.SafeMigrations.Tests.csproj --configuration Release
dotnet test tests/Doka.EntityFrameworkCore.SafeMigrations.MySql.Tests/Doka.EntityFrameworkCore.SafeMigrations.MySql.Tests.csproj --configuration Release
dotnet test tests/Doka.EntityFrameworkCore.SafeMigrations.PostgreSql.Tests/Doka.EntityFrameworkCore.SafeMigrations.PostgreSql.Tests.csproj --configuration Release
Docker is required for provider tests. CI additionally executes every supported engine profile, EF CLI/script/bundle paths, merged coverage thresholds, performance/allocation budgets, deterministic double-pack, isolated package-only consumers, and SPDX SBOM validation. FsCheck exercises generated Core and provider invariants with shrunk counterexamples, while the separate Dependency Review gate rejects newly introduced high-severity vulnerabilities and dependencies outside the approved license policy before merge.
Each provider matrix cell also persists a live full-runner latency artifact.
It measures 20 full-runner invocations after a warmup against 100 expected
tables before and after adding 1,000 foreign tables with child objects. Each
invocation may execute multiple database roundtrips. Expected assessments
must remain identical, foreign child rows must stay outside the scoped child
inventory, and noisy p95 must remain within 2 * clean p95 + 250 ms.
Release candidates and stable releases use the same manually dispatched path.
The workflow qualifies and attests exact bytes from current main before it
waits at the protected NuGet environment. Only then does the operator create a
signed annotated tag on that qualified commit and approve publication. The
write-capable job validates and cryptographically verifies the portable SLSA
bundle before using NuGet Trusted Publishing, verifies public repository
signatures and package content, and creates or verifies an immutable GitHub
Release with the exact six package files, checksums, SPDX manifest, and
release-provenance.intoto.jsonl. Candidates are marked prerelease and never
replace the latest stable release.
See Publication operations for the
step-by-step maintainer guide and current readiness, and
Release process for the qualification contract.
Design boundaries
SafeMigrations is not a destructive schema synchronizer. It does not infer renames, merge or split columns, narrow types, delete unknown objects, repair conflicting primary keys, or activate constraints over violating data. A classified rejection is part of the complete product contract.
Further documentation:
- Documentation index
- Public API reference
- Implementation design
- Vertical-slice architecture
- Support and qualification
- MySQL and MariaDB DDL behavior
- EF Core and provider upgrade boundary
- Sample project
Community and support
- Support channels and safe diagnostic sharing
- Contributing and verification requirements
- Code of Conduct
- Security policy and private vulnerability reporting
- Governance and responsibilities
- Project direction
- OpenSSF Best Practices evidence mapping
License
The product is MIT-licensed. See LICENSE. The adapted Code of Conduct is separately licensed under CC BY-SA 4.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Doka.EntityFrameworkCore.MySql (>= 10.4.0 && < 10.5.0)
- Doka.EntityFrameworkCore.SafeMigrations (>= 10.4.1)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.11 && < 10.1.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11 && < 10.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.4.1 | 32 | 9/12/2026 |
| 10.4.0 | 50 | 9/10/2026 |
| 10.3.2 | 75 | 9/10/2026 |
| 10.3.1 | 96 | 9/6/2026 |
| 10.3.0 | 92 | 9/6/2026 |
| 10.2.1 | 95 | 9/3/2026 |
| 10.2.0 | 89 | 9/2/2026 |
| 10.1.2 | 84 | 9/2/2026 |
| 10.1.1 | 89 | 9/1/2026 |
| 10.1.0 | 91 | 9/1/2026 |
| 10.0.2 | 106 | 8/31/2026 |
| 10.0.1 | 90 | 8/30/2026 |
| 10.0.0 | 98 | 8/29/2026 |
| 10.0.0-rc.3 | 66 | 8/29/2026 |
| 10.0.0-rc.2 | 59 | 8/29/2026 |
| 10.0.0-rc.1 | 66 | 8/28/2026 |