Doka.EntityFrameworkCore.SafeMigrations.Sqlite 10.5.0

dotnet add package Doka.EntityFrameworkCore.SafeMigrations.Sqlite --version 10.5.0
                    
NuGet\Install-Package Doka.EntityFrameworkCore.SafeMigrations.Sqlite -Version 10.5.0
                    
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.Sqlite" Version="10.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Doka.EntityFrameworkCore.SafeMigrations.Sqlite" Version="10.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Doka.EntityFrameworkCore.SafeMigrations.Sqlite" />
                    
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.Sqlite --version 10.5.0
                    
#r "nuget: Doka.EntityFrameworkCore.SafeMigrations.Sqlite, 10.5.0"
                    
#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.Sqlite@10.5.0
                    
#: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.Sqlite&version=10.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Doka.EntityFrameworkCore.SafeMigrations.Sqlite&version=10.5.0
                    
Install as a Cake Tool

Doka.EntityFrameworkCore.SafeMigrations

CI NuGet Core NuGet MySQL / MariaDB NuGet PostgreSQL NuGet SQLite NuGet SQL Server 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, PostgreSQL, SQLite, and SQL Server without assuming a common legacy migration history or deleting unknown objects. Each provider is a separate package; install only the adapter for the database engine the application uses.

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.5 operation-handler and typed migration-metadata SPI
  • Doka.EntityFrameworkCore.SafeMigrations.PostgreSql: PostgreSQL adapter on Npgsql 10
  • Doka.EntityFrameworkCore.SafeMigrations.Sqlite: SQLite adapter on the bundle-neutral official EF Core SQLite 10 provider core
  • Doka.EntityFrameworkCore.SafeMigrations.SqlServer: SQL Server adapter on the official EF Core SQL Server 10 provider

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
.Sqlite SQLite 3.46.1 or later through locked Microsoft.EntityFrameworkCore.Sqlite.Core; applications select and reference their native SQLite bundle, while qualification covers the default bundle in-memory and against asserted file databases
.SqlServer SQL Server 2019, 2022, and 2025; three separately pinned Linux/x86-64 container cells in CI

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.

Installation

Install one provider package. The core package is included transitively. The examples target prepared stable 10.5.0 and select that version exactly, so restore does not move to another version implicitly. Verify public package availability before installation; source preparation is not publication evidence. Release changes and compatibility notes belong in the changelog.

package_version='10.5.0'
dotnet package add Doka.EntityFrameworkCore.SafeMigrations.MySql --version "$package_version"

or:

package_version='10.5.0'
dotnet package add Doka.EntityFrameworkCore.SafeMigrations.PostgreSql --version "$package_version"

or:

package_version='10.5.0'
dotnet package add Doka.EntityFrameworkCore.SafeMigrations.Sqlite --version "$package_version"

or:

package_version='10.5.0'
dotnet package add Doka.EntityFrameworkCore.SafeMigrations.SqlServer --version "$package_version"

The SQLite adapter deliberately does not choose a native SQLite bundle. Add Microsoft.EntityFrameworkCore.Sqlite for the default bundle_e_sqlite3 deployment, or combine Microsoft.EntityFrameworkCore.Sqlite.Core with the application's selected SQLitePCLRaw provider and bundle for a custom native SQLite or SQLCipher deployment. The connected engine must report SQLite 3.46.1 or later.

SQL Server is optional: selecting another provider package does not introduce SQL Server runtime dependencies.

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();
});

SQLite registration is additive to UseSqlite:

using Doka.EntityFrameworkCore.SafeMigrations;
using Doka.EntityFrameworkCore.SafeMigrations.Sqlite;

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlite(connectionString);
    options.UseSqliteSafeMigrations();
});

SQL Server registration is additive to UseSqlServer:

using Doka.EntityFrameworkCore.SafeMigrations.SqlServer;

services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    options.UseSqlServerSafeMigrations<ApplicationDbContext>();
});

See SQL Server behavior for catalog permissions, provider-specific rejection boundaries, and supported engines. The EF migrator recovers session-scoped IDENTITY_INSERT after command failure or cancellation. A SQL-script client must recover the same session or discard it after an attention or timeout; SQL Server TRY/CATCH does not catch those client interruptions.

UseMySqlSafeMigrations() declares its user-variable requirement through the Doka 10.5 line. 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();

or:

services.AddEntityFrameworkSqlite();
services.AddSqliteSafeMigrations();

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>();
options.UseSqliteSafeMigrations<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 every 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();
// or: options.UseSqliteSafeMigrations();

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);
});

SQLite uses the same options contract:

using Doka.EntityFrameworkCore.SafeMigrations;

options.UseSqliteSafeMigrations(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 schema ensure/drop, table create/drop/rename, column add/alter/drop/rename, index create/drop/rename, standalone primary-key, unique, check, and foreign-key adds and drops, plus data changes derived from HasData. Add and alter columns use generated callbacks that retain the complete provider operation and its annotations before sealing the SafeMigrations definition. Constraint adds freeze ThrowIfDifferent; constraint drops make only absence idempotent. Unsupported operation annotations, an implicit foreign-key principal-column list, or check SQL outside the structured grammar stops scaffolding rather than losing semantics. Model-managed inserts, updates, and deletes become source-frozen SafeMigrations operations.

Existing migration source remains unchanged by this feature. SafeMigrations analyzes and guards only its own operations; every ordinary EF operation is handed to the provider unchanged and listed in the report as not analyzed, which invalidates the projected proofs that depend on it. Nothing reinterprets a published migration after the fact. Source that SafeMigrations cannot model is prevented where it is created, at scaffolding time.

SafeMigrations does not rewrite pending or replayed historical operations, including raw SQL, HasData data operations, and sequences. They continue through the configured provider's runtime generator. Only migrations scaffolded after the upgrade use the new safe builder calls.

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.

MariaDB can use session-local temporary catalog copies for large analyses; application tables and migration history remain untouched. Analysis retains the live-catalog path when temporary-table DDL is not permitted. Generated migration commands always read the live catalog. See catalog transport.

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 also accept a declared-domain-safe transition from VARCHAR into the TEXT family, a widening within that family, or a live-data-verified transition from a text type to VARCHAR(n). They 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.

Explicit AlterColumnIfDifferent and generated AlterColumnIfDifferentFromModel calls use the same provider-qualified transition families under RepairIfSafe, with an additional exact old-definition check. A missing or different source definition never authorizes an alteration; an already matching target remains a no-op. This applies to ordinary strict migrations as well as legacy-convergence streams. SQLite and SQL Server keep their own provider-specific alteration contracts.

SQL Server also supports explicit lossless tinyint -> smallint/int/bigint, smallint -> int/bigint, and int -> bigint alterations under RepairIfSafe. The exact source, unchanged defaults and identity semantics, supported storage, and absence of blocking dependencies must be proved. Drop dependent objects explicitly before changing their columns; recreate their reviewed target contracts afterward. Populated tables can receive simple nonthrowing integer CHECK predicates after a FALSE-only validation. See SQL Server transition boundaries.

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 normal MySQL/MariaDB index over a target text column must retain an explicit prefix; a full key blocks repair. A MySQL/MariaDB character 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'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 unless a structurally proven non-null default permits Doka's guarded backfill. 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.

A structurally proven non-null default is a non-null literal or a parsed, typed SQL expression whose shape proves a non-null result, such as a current value or COALESCE with a proven non-null argument. Raw SQL text, casts, binary arithmetic, and ordinary column references do not provide that proof. For an explicit MySQL/MariaDB type-changing alteration with default backfill, non-nullness alone is insufficient: the replacement must be a supported literal that fits both the old and target domains, with strict conversion enabled.

SQLite uses the official provider's model-owned rebuild path for changes that the engine cannot alter directly. A rebuild is accepted only when the terminal model owns every live and target column, index, primary key, unique constraint, check, and foreign key. Unknown triggers, referencing views, virtual tables, expression or partial indexes, STRICT, and WITHOUT ROWID reject before DDL. Table drops project external incoming foreign keys across ordered creates, renames, replacements, and drops; self-references disappear with their table. For a rebuild, foreign-key enforcement is disabled before the local transaction starts, the official provider rebuild runs inside that transaction, and final postconditions must pass before commit. When enforcement was enabled before the rebuild, the affected relationship closure must also pass foreign_key_check. The original mode is restored afterwards. The rebuild unit is atomic; EF's separate migration-history write is not part of that local transaction. See SQLite behavior.

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

Sparse ordered operations can combine preceding accepted additions with independently captured unchanged live members. Adding a nullable FK column does not require redundant convergence of its existing principal. Native reference keys, endpoint comparison semantics, surviving physical identities and row safety still qualify separately. Defaults, generated values, future or rejected additions cannot impersonate an accepted NULL-preserving addition. Runtime guards and native constraint creation remain authoritative. See sparse live prerequisites.

Runtime guards recheck fresh live metadata and required data proofs when each safe operation executes, including when explicit preflight is disabled.

PostgreSQL also checks native operator-class and access-method eligibility for physical index-key types. An unsupported ordinary Btree key on json is rejected before DDL or unique-data probes; supported jsonb retains its native contract. INCLUDE columns and genuinely computed results are separate concerns.

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 also covers ordinary EF and provider operations from existing or manually authored migrations. SafeMigrations does not analyze their effects; this status is not their safety approval and requires independent artifact and postcondition review. 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.

RuntimeValidationRequired is not accepted by this safe-only example: review the preceding SQL and its deployment effects separately, then rely on the later safe operations' runtime guards in the approved execution path.

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. A rename owns both its source and destination identities for final-writer selection, but its own postcondition 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. The current report schema v4 retains separate provider AnalysisCode and policy DecisionCode, bounded typed facet differences, and 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 EF operations in published migrations retain their provider-owned behavior and contribute only their CLR type; their SQL and properties are not captured by this fingerprint. Newly scaffolded LegacyConvergence source may use a different policy and fingerprint from strict source. Retain the immutable artifact digest and independent review for every provider-owned operation. Serialize with SafeMigrationReportJson; the package includes the current safe-migration-run-report-v4 schema. The version 1 and version 2 and version 3 schemas remain available for readers of previously persisted reports.

When pending migrations contain raw SQL followed by safe operations, read-only preflight cannot infer the SQL effect from the initial catalog snapshot. Later safe operations whose state is therefore unprovable have action ValidateAtRuntime, no claimed observed state or postcondition, and a DeferredOrigin identifying the SQL operation and its owning migration. The aggregate status is RuntimeValidationRequired unless another operation is definitively blocked; invariantly unsupported contracts remain blocked even after raw SQL. ThrowIfBlocked() permits this status, but it is not a proof that the remaining operations will succeed: each safe operation is catalog- and data-checked again when execution reaches it. Raw SQL itself remains provider-owned and unanalyzed. On MySQL/MariaDB, earlier DDL can already be committed if a later guard rejects the migration.

Model-managed seed operations also use runtime validation when preceding unconfined writes or discarded row evidence invalidate their read-only proof. This includes ordered child/parent deletes: old dependency counts cannot account for trigger side effects. DeferredOrigin identifies the preceding invalidation. Direct guarded row postconditions and confined inserts into newly created plain tables retain their proven classifications; untouched conflicts still block.

SQL Server also records a DDL origin when an enabled DDL trigger or insufficient database/server metadata visibility prevents trusting later row or structural evidence. Affected assessments use the same runtime-validation contract; lack of server visibility is not a trigger-absence proof or an automatic permission grant. Runtime validates immediately before the affected operation, not in the background. Independent invariant blockers remain blocking. An initial matching no-op emits no DDL and does not invalidate evidence, but metadata captured before earlier executable trigger-risk DDL cannot certify a later no-op. After a structural operation is deferred, later supported operations retain runtime validation without an invented physical postcondition. See SQL Server row-proof freshness.

PostgreSQL applies the same boundary after DDL when an event trigger is active for the session, or catalog access cannot prove its absence. Trigger functions can change unrelated rows and objects; the initiating object's postcondition does not establish global freshness. Disabled or inactive replica-only triggers do not discard ordinary proofs. See PostgreSQL event triggers.

Both providers also invalidate captured structure after executable DML when a user DML trigger can execute unrelated DDL. A global presence flag includes indirect FK-cascade targets without retaining trigger bodies or an owner graph. The report identifies the actual DML origin. Empty typed writes and initial safe no-ops do not activate this boundary; runtime and independent blockers remain authoritative.

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-v3 schema; the existing one-argument serializer remains the canonical complete report v4. The version 1 and version 2 view schemas remain available for previously persisted views. NonMatching retains deferred assessments and their origins; BlockingOnly does not present them as proven blockers.

Assessments include explicit table/schema and relationship identity, readable explanations and bounded text summaries via SafeMigrationReportText. Runtime diagnostics also work without preflight. See errors and readable reports for usage and compatibility.

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 metadata on a MySQL/MariaDB table is a database qualifier. SafeMigrations accepts an explicit qualifier only when it exactly equals DATABASE() for the active connection. That qualifier and an omitted qualifier share one physical identity during catalog analysis, ordered projection, DDL, and model-managed data, including postflight final-writer reduction and generated transition catalogs. Because SQL generation has no connection, a generated stream merges a sole explicit qualifier with omitted qualifiers only while applying that qualifier as a runtime guard to every SafeMigrations command in the stream. A foreign or ambiguous stream therefore fails before its first SafeMigrations mutation. EnsureSchema for the selected database is an idempotent no-op. A different qualifier fails with database_qualifier_mismatch before prerequisite or data queries, and database drops remain unsupported. Database identity uses exact casing rather than the environment-specific lower_case_table_names behavior. No qualifier is silently removed or redirected to the current database.

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. SQL Server scaffolding preserves included non-key index columns through the public CreateIndexWithIncludesIfNotExistsFromModel and CreateCompositeIndexWithIncludesIfNotExistsFromModel operations; filtered and descending index facets remain part of the reviewed index definition. 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.
  • SQLite safe structural segments that do not rebuild a table use the caller transaction or one local transaction. Rebuild segments reject an existing caller transaction and own a transaction-suppressed foreign-key boundary plus one local transaction. Runtime migration and Migration Bundles are supported. SQL scripts containing safe operations reject before partial output because SQLite cannot encode the required catalog-dependent guards.
  • 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 server-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. SQLite separately qualifies 50,000 mixed model-managed row transitions and budgets live analysis on 1,000 tables carrying indexes and foreign keys. These workloads reflect SQLite's in-process catalog and rebuild execution model rather than claiming equivalence with the server-provider transport matrix.

Bounded catalog transport does not bound application-row scans or DDL duration. Required data scans and DDL can still dominate large-table workloads. See MySQL and MariaDB behavior for resource bounds and command-timeout semantics.

See Deployment and recovery and Failure codes.

Build and qualification

The SDK is fixed by global.json.

dotnet restore Doka.EntityFrameworkCore.SafeMigrations.slnx
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
dotnet test tests/Doka.EntityFrameworkCore.SafeMigrations.Sqlite.Tests/Doka.EntityFrameworkCore.SafeMigrations.Sqlite.Tests.csproj --configuration Release
dotnet test tests/Doka.EntityFrameworkCore.SafeMigrations.SqlServer.Tests/Doka.EntityFrameworkCore.SafeMigrations.SqlServer.Tests.csproj --configuration Release

Docker is required for server-provider tests. SQL Server live tests additionally require a supported x64 host; ARM64 runs skip them and cannot establish live qualification. CI additionally executes every supported engine profile, EF CLI/script/bundle paths, merged coverage thresholds, informational performance/allocation reports, 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.

Benchmark budget overruns do not block CI or release qualification on shared hardware. All five sets retain measured durations, allocations, comparison limits, and verdicts; invalid configuration or failed benchmark execution still blocks. See Support and qualification.

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, and foreign child rows must stay outside the scoped child inventory. The unchanged 2 * clean p95 + 250 ms comparison is informational: timing overruns retain a false JSON budget verdict without failing qualification. Invalid measurements, execution and artifact-write failures remain blocking.

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 ten 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.5.0 94 10/15/2026
10.4.11 88 10/14/2026
10.4.10 87 10/13/2026
10.4.9 145 10/8/2026
10.4.8 102 10/6/2026
10.4.7 93 10/6/2026
10.4.6 103 10/5/2026
10.4.5 124 9/30/2026
10.4.4 100 9/26/2026
10.4.3 105 9/23/2026
10.4.2 108 9/19/2026