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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Doka.EntityFrameworkCore.SafeMigrations.MySql" Version="10.4.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Doka.EntityFrameworkCore.SafeMigrations.MySql" Version="10.4.1" />
                    
Directory.Packages.props
<PackageReference Include="Doka.EntityFrameworkCore.SafeMigrations.MySql" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Doka.EntityFrameworkCore.SafeMigrations.MySql --version 10.4.1
                    
#r "nuget: Doka.EntityFrameworkCore.SafeMigrations.MySql, 10.4.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Doka.EntityFrameworkCore.SafeMigrations.MySql@10.4.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Doka.EntityFrameworkCore.SafeMigrations.MySql&version=10.4.1
                    
Install as a Cake Addin
#tool nuget:?package=Doka.EntityFrameworkCore.SafeMigrations.MySql&version=10.4.1
                    
Install as a Cake Tool

Doka.EntityFrameworkCore.SafeMigrations

CI NuGet Core NuGet MySQL / MariaDB NuGet PostgreSQL License: MIT OpenSSF Scorecard OpenSSF Best Practices

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, and MigrationBuilder extensions
  • Doka.EntityFrameworkCore.SafeMigrations.MySql: MySQL and MariaDB adapter on the public Doka.EntityFrameworkCore.MySql 10.4.0 operation-handler and typed migration-metadata SPI
  • Doka.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 ROUTINE privilege.
  • 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 RepeatableRead transaction and transaction-scoped advisory lock. If the caller already owns a transaction, it must be read-only and use RepeatableRead or Serializable; 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:

Community and support

License

The product is MIT-licensed. See LICENSE. The adapted Code of Conduct is separately licensed under CC BY-SA 4.0.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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