Keep migrations in C#
++ Migrator: imperative and fluent schema operations, scoped + history, tags/profiles, and the CLI or your own host. + FluentMigrator: a fluent DSL with packaged + runners, tags and profiles. +
+diff --git a/.gitattributes b/.gitattributes
index dfdb8b77..e77eddf1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,4 @@
*.sh text eol=lf
+docs/index.html linguist-generated=true
+docs/guide/*.html linguist-generated=true
+docs/assets/search-index.json linguist-generated=true
diff --git a/.github/scripts/build-docs.py b/.github/scripts/build-docs.py
new file mode 100644
index 00000000..58cd0aba
--- /dev/null
+++ b/.github/scripts/build-docs.py
@@ -0,0 +1,140 @@
+"""Build the dependency-free documentation site. Generated HTML is committed for easy preview."""
+import argparse
+import html
+import hashlib
+import importlib.util
+import json
+from pathlib import Path
+import re
+
+ROOT = Path(__file__).resolve().parents[2]
+DOCS = ROOT / "docs"
+spec = importlib.util.spec_from_file_location("documentation_content", DOCS / "_src/content.py")
+content = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(content)
+
+
+def slug(value):
+ return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
+
+
+def highlight(code, kind):
+ if kind == "shell":
+ return html.escape(code)
+ tokens = re.compile(r'//[^\n]*|"(?:\\.|[^"\\])*"|\b(?:using|var|new|public|class|static|override|void|if|foreach|in|return|throw|false|true|null|typeof|params|string|object|int)\b|\b\d+\b')
+ result, end = [], 0
+ for match in tokens.finditer(code):
+ result.append(html.escape(code[end:match.start()]))
+ value = match.group()
+ css = "comment" if value.startswith("//") else "string" if value.startswith('"') else "number" if value.isdigit() else "keyword"
+ result.append(f'{html.escape(value)}')
+ end = match.end()
+ result.append(html.escape(code[end:]))
+ return ''.join(result)
+
+
+def example_html(example, instance):
+ eid = slug(instance + "-" + example["id"])
+ label = "Inside Up() / BuildUp(MigrationBuilder migration)" if example["kind"] == "body" else "Shared commands · both styles" if example["kind"] == "shell" else "Shared host · both styles" if example["classic"] == example["fluent"] else "Choose one authoring style"
+ controls = ''.join(f'' for style in ("classic", "fluent"))
+ panels = []
+ for style in ("classic", "fluent"):
+ panels.append(f'''{style.title()}
{highlight(example[style], example['kind'])}
{label}
{html.escape(page['summary'])}
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
{''.join(sections)}{html.escape(page["summary"])}
DOTNETPROJECTS / THE MIGRATION MANUAL
From your first table to deployment locks and SQLite reconstruction. Practical guides with a Classic and Fluent example for every authoring task.
Start with a working example ↗THE .NET MIGRATION LANDSCAPE
+
+ Feature comparison · Reviewed 23 September 2026
Read the sources and qualifications ↓
+
+ Use fluent operations, version planning, a SQL-preview subset, runner options, + native locks and the CLI. + Read the runner guide and provider limits. +
++ Migrator fits applications that want explicit C# migrations and + scoped history without coupling schema changes to an ORM. Other + tools offer different authoring and deployment workflows. +
+
+ Read the detailed feature comparison (Markdown) →
+ Explore SQLite emulation, preservation limits and framework
+ differences →
+
+ Scroll horizontally to compare all five frameworks on smaller + screens. +
+| Capability | ++ Migrator.NET DotNetProjects forkSource [1] + | ++ FluentMigrator Sources [2] + | ++ EF Core Sources [3] + | ++ DbUp Sources [4] + | ++ Evolve Sources [5] + | +
|---|---|---|---|---|---|
| Authoring style | +Imperative C# + structured fluent API | +Handwritten C# Fluent DSL |
+ C# generated from model changes; editable | +SQL scripts; C# scripts also supported | +Versioned SQL files | +
| ORM-independent workflow | +Yes | +Yes | +Uses EF model and DbContext | +Yes | +Yes | +
| + Generate migrations from model differences + | +No built-in generator | +Hand-authored | +Yes — model snapshots | +Hand-authored | +Hand-authored | +
| Raw SQL | +ExecuteNonQuery / fluent Execute.Sql / scripts |
+ Execute.Sql / scripts |
+ migrationBuilder.Sql |
+ Primary workflow | +Primary workflow | +
| Downgrade an applied version | +
+ Authored Down() or supported automatic reversal
+ |
+
+ Down(); auto-reverse for supported expressions
+ |
+ Down(); target an earlier migration |
+ Forward fixes; custom undo workflow | +Forward fixes; no Down command | +
| History / module separation | ++ Scope-filtered discovery + history + | +Custom version tables + migration filtering | ++ Separate contexts / migrations + custom history tables + | +Separate journals + script filtering | +Metadata table/schema + script locations | +
| Transactions | +Per migration by default; none or whole session (SQLite, PostgreSQL, SQL Server) | +Per migration by default; configurable | +Most migrations wrapped automatically | +Opt-in per script or whole run; none by default | +Per migration by default; whole-run option | +
| Execution / deployment | +Library + CLI | +In-process runner + CLI | +CLI, SQL scripts, bundles, runtime API | +Library; host in a console app or application | +.NET library, .NET tool, CLI | +
| Database abstraction | +Provider dialects for schema operations | +Provider-specific SQL generators | ++ Relational providers; migrations may differ by provider + | +Database integrations; you write dialect-specific SQL | +Database integrations; you write dialect-specific SQL | +
| Repeatable / recurring work | +Ordered maintenance + named profiles; no checksum repeatables | +Maintenance migrations / profiles | +Seeding APIs (EF 9+); custom code | +RunAlways scripts |
+ Repeatable SQL reruns on checksum change | +
| Automatic SQLite reconstruction | +Live-schema rebuilds; no ORM model | +Manual for general column and foreign-key alterations | +Rebuilds for model-represented artifacts | +Author scripts | +Author scripts | +
| Planning and SQL preview | +Read-only version plan; connected/offline SQL subset | +Preview/output | +Generated SQL scripts | +Authored SQL / pending scripts | +Authored SQL | +
| Deployment coordination | +Opt-in native locks: SQL Server, PostgreSQL, MySQL/MariaDB | +Application-lock pattern / deployment orchestration | +Migration locking; execution-path dependent | +Host/provider concern | +Cluster setting; provider-dependent | +
+ Rollback has two meanings. Reversing an already + applied migration uses authored reverse operations. Rolling back a + failed transaction depends on the database’s DDL support. Neither + restores data removed by a successful destructive migration. +
++ Recurring work is not the same as change detection. + Evolve stores script checksums and validates changes; Migrator + records versions and scopes without built-in content checksum + validation. Maintenance hooks, seeding and RunAlways have + different execution rules. +
++ Migrator: imperative and fluent schema operations, scoped + history, tags/profiles, and the CLI or your own host. + FluentMigrator: a fluent DSL with packaged + runners, tags and profiles. +
++ EF Core: a natural fit when an EF model defines + your schema and you want migration scaffolding, SQL generation + and deployment bundles. +
++ DbUp: compose a script runner in .NET. + Evolve: convention-based versioned SQL, + checksum validation and repeatable scripts. +
+
+ Our column is based on the current repository source, which
+ targets net9.0. Other columns summarize official
+ documentation reviewed on 22 September 2026, with SQLite comparisons rechecked on 23 September, rather than claiming
+ parity across every released package. Check your chosen release,
+ provider and database version. Suitability notes are our
+ interpretation of these documented capabilities.
+
| {escape(h)} | ' for h in headers) + '
|---|
| {v} | ' for v in row) + '
Start with the .NET 9 SDK and a console application. The core package supplies schema operations; your ADO.NET package connects to the database. SQLite needs no separate database server for this example.
', INSTALL), + section("Write the change", 'Add CreateUsers.cs. Choose one tab and copy that class. Each public migration has a numeric version; do not put both versions of the same example into one assembly. Classic migrations execute provider methods in Up and Down. Fluent migrations collect structured operations in BuildUp and BuildDown.
Replace Program.cs with the shared host below and run dotnet run. The open connection belongs to this host and is disposed after the provider. The runner discovers public migration classes in the selected assembly.
The result is an app.db file containing Users and SchemaInfo. Run again: version 1 is already recorded, so it is skipped. Add a class with [Migration(2)] for your next change.
Call runner.MigrateTo(0) to execute the reverse methods for this migration set. Here that drops Users and its data. A reverse migration is a schema operation, not a restore of deleted rows. Test both directions on a disposable database before deployment.
Continue with creating tables, or configure scopes, filters and transaction behavior.
'), source="examples/FluentQuickStart/Program.cs") + +page("Introduction", "installation", "Installation", "The core library, database driver, optional DI integration and CLI each have a distinct job.", + section("Choose your packages", table(["Package", "Purpose"], [["DotNetProjects.Migrator", "Migration classes, providers, runner and fluent operations."], ["An ADO.NET driver", "Install the driver for the database your host opens."], ["DotNetProjects.Migrator.Extensions.DependencyInjection", "Optional scoped runner, constructor injection and Microsoft logging."], ["DotNetProjects.Migrator.Tool", "Themigrator command-line tool."]]), INSTALL),
+ section("Choose a driver", 'Common choices are Microsoft.Data.Sqlite, Microsoft.Data.SqlClient, Npgsql, MySql.Data, Oracle.ManagedDataAccess.Core and FirebirdSql.Data.FirebirdClient. The core library does not directly reference these packages. Passing an open connection makes driver selection explicit and keeps connection ownership with your application.
Read the provider overview for database families, aliases and CI coverage. A provider name is not a guarantee that every native operation has the same behavior on every server.
'), + section("Use the repository", 'To develop against a checkout, replace the core package reference with a project reference to src/Migrator/DotNetProjects.Migrator.csproj. The solution targets .NET 9. Building the .slnx solution requires an SDK that understands that format, such as SDK 9.0.200 or later.
Keep the library, CLI and optional DI integration on compatible versions. Recompile old migration assemblies when updating a breaking API; the upgrade guide explains the column and constraint changes.
'), source="src/Migrator/DotNetProjects.Migrator.csproj") + +page("Introduction", "configuration", "Configuration", "Select the connection, migration set and history scope first, then set runner options before executing.", + section("Connect and select a scope", 'This host fragment assumes an open ADO.NET connection. The provider scope partitions history and selects explicitly scoped classes. An unscoped migration inherits the provider scope. Scopes do not create separate database objects: two modules can still conflict on a table name.
Set the history table before accessing history or running migrations. Its default name is SchemaInfo; the default scope is default. Give each runner its intended assembly or explicit migration types. Duplicate versions within an effective scope fail discovery.
Load connection strings from application configuration or environment variables. The CLI reads MIGRATOR_CONNECTION by default. Do not store production credentials in migration classes.
No. Migrations operate on an ADO.NET connection through the transformation provider. Use EF, Dapper, another data layer or direct SQL in the rest of your application. Migrator does not scaffold schema changes from an object model.
'), + section("Can I mix Classic and Fluent?", 'Yes. Both implement the same migration contract, run through the same loader and share history. Keep each version unique. The tabs throughout these guides show equivalent choices, not two classes to install together. The authoring method names differ: Up/Down versus BuildUp/BuildDown.
SQLite does not implement every ALTER TABLE operation. Migrator reads the live schema and reconstructs a supported table when a column or constraint change needs it. This is useful without an ORM model. See SQLite for preserved objects, foreign-key checks and reconstruction boundaries.
'), + section("Does rollback recover data?", 'A transaction can roll back a failed migration when its database operations are transactional. Downgrading a completed version executes your reverse method. Neither mechanism recovers rows already deleted by a successful migration. Use an explicit recovery design and backups for that case.
'), + section("Can I edit an applied migration?", 'The journal records versions, scopes and timestamps, not a content checksum. Editing an applied class will not make it rerun. Add a new migration for a change. Consolidated baselines are an explicit history operation; read versioning and history.
'), + section("Why does SQL preview reject my migration?", 'Preview renders a structured subset. A provider callback, unsupported constraint alteration or schema dependency after raw SQL cannot be represented reliably and raises an error. Read planning and SQL preview rather than treating preview as a full execution simulation.
')) + +page("Operations", "creating-tables", "Creating tables", "Describe a complete table: columns first, with explicit named keys and constraints.", + section("Create a table with a key", 'The table definition groups related schema objects into one operation. Primary-key columns are emitted as non-nullable. In the fluent API a complete table is collected before execution, so keys can refer to columns declared in the same chain.
', CREATE_USERS), + section("Composite keys and uniqueness", 'Use the declared key order consistently in both primary and foreign keys. A composite unique constraint applies to the tuple; it does not make each column unique separately. The fully qualified constraint type below avoids the name collision with System.Data.UniqueConstraint.
', pair("A table with an ordered composite key", ''' +Database.AddTable("Subscriptions", + new Column("TenantId", DbType.Int32), + new Column("UserId", DbType.Int32), + new Column("Email", DbType.String, 255), + new PrimaryKeyConstraint("PK_Subscriptions", "TenantId", "UserId"), + new DotNetProjects.Migrator.Framework.UniqueConstraint( + "UQ_Subscriptions_Email", "TenantId", "Email")); +''', ''' +migration.Create.Table("Subscriptions") + .WithColumn("TenantId").AsInt32() + .WithColumn("UserId").AsInt32() + .WithColumn("Email").AsString(255) + .WithPrimaryKey("PK_Subscriptions", "TenantId", "UserId") + .WithUniqueConstraint("UQ_Subscriptions_Email", "TenantId", "Email"); +''', smoke=True)), + section("Identity and removal", 'Identity generation is a column attribute, separate from primary-key membership. SQLite requires an INTEGER identity column and its single-column primary key in the same definition. Use a complete Create.Table/AddTable operation to satisfy that rule. To reverse creation use Database.RemoveTable or migration.Delete.Table; dropping a table also removes its rows.
For supported creation operations, automatic reversal can derive the reverse operation. Explicitly author reverse behavior for destructive changes.
')) + +page("Operations", "altering-tables", "Altering tables", "Rename objects and evolve populated tables while preserving the schema details you still need.", + section("Rename a table and column", 'Use explicit old and new names. The column rename signature is table, old name, new name in both APIs. A table rename does not rename explicit constraints or their backing indexes. Reusing the original key name for a replacement table may collide on SQL Server or PostgreSQL.
', pair("Rename existing objects", ''' +Database.RenameTable("Users", "Members"); +Database.RenameColumn("Members", "Name", "DisplayName"); +''', ''' +migration.Rename.Table("Users", "Members"); +migration.Rename.Column("Members", "Name", "DisplayName"); +''')), + section("Change a complete column definition", 'Supply the type, length, nullability, default and collation you intend to retain. ChangeColumn replaces the column definition; it does not infer that table constraints should be created or removed. Existing rows must remain valid for the new definition.
', pair("Widen a required display name", ''' +Database.ChangeColumn("Users", new Column("Name", DbType.String, 500) +{ + IsNullable = false +}); +''', ''' +migration.Alter.Column("Name", "Users") + .AsString(500).NotNullable(); +''')), + section("A deployment sequence for populated data", 'Add a nullable column, deploy code that can read both forms, backfill values, then enforce the final requirement in a later migration. Large data copies and index creation can hold locks for substantial time; test them against a representative dataset.
On SQLite, a supported alteration may recreate the table and copy rows. On Oracle and some other engines, DDL may commit implicitly. Review the transaction guide and your provider page before choosing the deployment boundary.
')) + +page("Operations", "columns", "Columns and data types", "Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.", + section("Add and remove a column", 'Column builders take column name followed by table name. Classic AddColumn takes table name first. New nullable columns accept existing rows without a backfill. A required column usually needs a compatible default or a staged data migration.
', pair("Add an optional email address", ''' +Database.AddColumn("Users", new Column("Email", DbType.String, 320)); +''', ''' +migration.Create.Column("Email", "Users").AsString(320).Nullable(); +'''), pair("Remove the email column", ''' +Database.RemoveColumn("Users", "Email"); +''', ''' +migration.Delete.Column("Email", "Users"); +''')), + section("Precision and defaults", 'For decimal values specify precision and scale. In a Column constructor an integer after the type is the size, not a numeric default. Set DefaultValue explicitly to avoid overload ambiguity. Plain strings are values; trusted SQL expressions use RawSql.Insert.
', pair("An amount with four decimal places", ''' +Database.AddColumn("Orders", new Column("Amount", DbType.Decimal) +{ + Precision = 12, Scale = 4, IsNullable = false, DefaultValue = 0m +}); +''', ''' +migration.Create.Column("Amount", "Orders").OfType(DbType.Decimal) + .WithPrecision(12, 4).NotNullable().WithDefaultValue(0m); +''')), + section("Time of day and durations", 'Use TimeOnly for time-of-day values and TimeSpan for intervals. A TimeSpan is a duration, including negative and multi-day values, so a TimeSpan default on a Time column is rejected. PostgreSQL and Oracle have native intervals; SQLite, SQL Server and MySQL/MariaDB store intervals as signed .NET ticks.
', pair("Clock time and elapsed time", ''' +Database.AddTable("Jobs", + new Column("RunAt", DbType.Time) { DefaultValue = new TimeOnly(9, 30) }, + new Column("Elapsed", MigratorDbType.Interval) { DefaultValue = TimeSpan.Zero }); +''', ''' +migration.Create.Table("Jobs") + .WithColumn("RunAt").OfType(DbType.Time).WithDefaultValue(new TimeOnly(9, 30)) + .WithColumn("Elapsed").OfType(MigratorDbType.Interval).WithDefaultValue(TimeSpan.Zero); +''', smoke=True)), + section("Database storage differs", 'SQLite does not enforce declared string lengths or decimal precision. UInt64 values above Int64.MaxValue are rejected there. Oracle character empty strings become NULL; Informix and Sybase have their own trimming and range behavior. Consult the type support and boundary matrix for supported mappings and live-test scope. A shared DbType does not imply identical native storage.
')) + +page("Operations", "data", "Data operations", "Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.", + section("Insert rows", 'Column and value arrays must have the same length. The provider binds values using its driver-specific parameter mappings. For multiple rows issue multiple operations; the fluent Row method describes one row, not an accumulated collection of rows.
', pair("Insert a user", ''' +Database.Insert("Users", new[] { "Id", "Name" }, new object[] { 1, "Ada" }); +''', ''' +migration.Insert.IntoTable("Users") + .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" }); +''')), + section("Update and delete with predicates", 'Without a predicate, update/delete affects every row. Supply predicate columns and values deliberately. Fluent WhereSql is available for updates only; its text is trusted SQL, not an escaped user input.
', pair("Update one user", ''' +Database.Update("Users", new[] { "Name" }, new object[] { "Ada Lovelace" }, + new[] { "Id" }, new object[] { 1 }); +''', ''' +migration.Update.Table("Users") + .Set(new[] { "Name" }, new object[] { "Ada Lovelace" }) + .Where(new[] { "Id" }, new object[] { 1 }); +'''), pair("Delete one user", ''' +Database.Delete("Users", new[] { "Id" }, new object[] { 1 }); +''', ''' +migration.Delete.FromTable("Users").Where(new[] { "Id" }, new object[] { 1 }); +''')), + section("Conditional seed data", 'Use an explicit identifying predicate when a seed should exist only once. This is distinct from a migration version: a named profile can run repeatedly without a history entry. Coordinate competing writers; a check-then-insert helper is not a substitute for a database unique key.
', pair("Insert a missing seed", ''' +Database.InsertIfNotExists("Users", new[] { "Id", "Name" }, + new object[] { 1, "Ada" }, new[] { "Id" }, new object[] { 1 }); +''', ''' +migration.Insert.IntoTable("Users") + .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" }) + .IfNotExists(new[] { "Id" }, new object[] { 1 }); +''')), + section("Copying and reversal", 'Use the provider CopyDataFromTableToTable helper or fluent Execute.CopyData for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateFrom maps source/target pairs. These operations retain provider limits and are outside the SQL-preview subset. A reverse data migration needs authored recovery logic; auto-reversal cannot recreate deleted or overwritten values.
', pair("Copy users into an archive table", ''' +Database.CopyDataFromTableToTable("Users", + new System.Collections.Generic.ListClassic migrations read through Database. FluentMigration exposes Schema for queries and Context for the full provider API. A fluent authoring method runs before its queued operations: an inspection cannot see a table merely queued earlier in the same builder.
', pair("Add a column only when it is missing", ''' +if (!Database.ColumnExists("Users", "Email")) + Database.AddColumn("Users", new Column("Email", DbType.String, 320)); +''', ''' +if (!Schema.Table("Users").ColumnExists("Email")) + migration.Create.Column("Email", "Users").AsString(320); +''')), + section("Read ordered constraints", 'GetColumns returns inferred column attributes, not primary/unique membership flags. It is obsolete because native types and defaults cannot be mapped back to exact .NET definitions; use migration history for the original definition. Read typed table constraints to retain ordered composite keys. Unique indexes remain index metadata. MySQL/MariaDB catalogs cannot distinguish every original unique-index versus UNIQUE-clause authoring choice.
', pair("Read table constraint definitions", ''' +var constraints = Database.GetTableConstraints("Users"); +foreach (var constraint in constraints) + Console.WriteLine(constraint.Name); +''', ''' +var constraints = Schema.Table("Users").ConstraintDefinitions(); +foreach (var constraint in constraints) + Console.WriteLine(constraint.Name); +''')), + section("Create a view", 'ViewField selects columns from a base table. The alternative IViewElement overload represents explicit columns and joins. View definitions are provider-dependent and outside SQL preview and automatic reversal. Write a provider-appropriate DROP VIEW statement in the reverse method, and manage dependent views when changing their underlying tables.
', pair("A projection over Users", ''' +Database.AddView("UserNames", "Users", new ViewField("Id"), new ViewField("Name")); +''', ''' +migration.Create.View("UserNames", "Users", new ViewField("Id"), new ViewField("Name")); +''')), + section("Reads and portability", 'Dispose readers and commands obtained from the provider. Fluent Schema.Query and Select accept a reader callback and handle disposal. Use provider quoting helpers for table and column identifiers separately: quoting a table may introduce schema qualification, which is not valid for a column expression.
Metadata fidelity depends on the provider. Unsupported readers throw instead of pretending that an empty schema was found. A successful existence check is not a full schema-drift report.
'), source="src/Migrator/Framework/Fluent/FluentMigration.cs") + +page("Operations", "sql", "Execute SQL and scripts", "Use schema operations where they fit, and keep database-specific SQL explicit.", + section("Execute a statement", 'Raw SQL passes through to the selected database. It does not translate between dialects. Values from application input should be bound through a command; migration SQL is trusted application code.
', pair("A SQL data change", ''' +Database.ExecuteNonQuery("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL"); +''', ''' +migration.Execute.Sql("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL"); +''')), + section("Files and embedded resources", 'ExecuteScript reads a file; ExecuteResourceScript reads an assembly resource. Fluent equivalents capture script text as dedicated operations. Make files available at deployment and set resource names explicitly. Relative file paths are resolved against the process working directory.
', pair("Execute a SQL file", ''' +Database.ExecuteScript("Scripts/backfill.sql"); +''', ''' +migration.Execute.Script("Scripts/backfill.sql"); +'''), pair("Execute an embedded SQL resource", ''' +Database.ExecuteResourceScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql"); +''', ''' +migration.Execute.EmbeddedScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql"); +'''), 'For the second example mark backfill.sql as an EmbeddedResource in the migration project and use its actual manifest resource name. Missing resources fail before script execution.
'), + section("Client batch separators", 'The script APIs split standalone SQL Server GO lines, including optional line comments, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail before any batches execute. ExecuteNonQuery and Execute.Sql do not split client separators.
Other providers receive one command unless they implement IScriptBatchProvider. A database SQL file is not necessarily compatible with SQL*Plus, mysql-client or isql command syntax. Raw SQL also invalidates planned schema knowledge during SQL preview.
')) + +page("Operations", "connections", "Commands and callbacks", "Use the active provider connection when a migration needs driver-level work.", + section("Bind command parameters", 'The provider creates a command associated with its current transaction. Dispose it after use. Generate parameter names through the provider, rather than assuming every driver uses the same convention. The callback is deferred until fluent execution reaches it.
', pair("Execute a parameterized command", ''' +using var command = Database.CreateCommand(); +var name = Database.GenerateParameterName(0); +command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1"; +var value = command.CreateParameter(); +value.ParameterName = name; +value.Value = "Ada"; +command.Parameters.Add(value); +command.ExecuteNonQuery(); +''', ''' +migration.Execute.WithProvider(provider => +{ + using var command = provider.CreateCommand(); + var name = provider.GenerateParameterName(0); + command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1"; + var value = command.CreateParameter(); + value.ParameterName = name; + value.Value = "Ada"; + command.Parameters.Add(value); + command.ExecuteNonQuery(); +}); +''')), + section("Connection ownership", 'WithCommand creates and disposes a provider command around your action. WithConnection exposes the connection; WithProvider exposes the complete transformation provider. Do not close or replace a runner-owned connection, commit its transaction or switch databases while holding a native migration lock.
'), + section("Database administration", 'Database creation and other administration require a connection and identity authorized for that operation. Use a dedicated host with TransactionMode.None. Fluent administration rejects an active transaction; do not combine it with WholeSession or assume a Classic provider call can participate in transactional DDL.
', pair("Create a database on a supporting server", ''' +Database.CreateDatabases("Reporting"); +''', ''' +migration.Administration.CreateDatabase("Reporting"); +'''), 'The remaining mappings are DropDatabases / Administration.DropDatabase, SwitchDatabase / Administration.SwitchDatabase, and KillDatabaseConnections / Administration.KillConnections. These are explicit administrative actions with provider-specific support. Database switches invalidate assumptions about migration history and session locks: keep provisioning separate from ordinary schema migrations. They are outside SQL preview and automatic reversal.
'), + section("Preview and reversal", 'Callbacks can perform arbitrary C# work and cannot be translated into SQL preview. They require explicit reverse behavior. Keeping external network calls out of migration bodies makes failures easier to reason about: a database rollback cannot undo an email or an HTTP request.
')) + +page("Schema basics", "indexes", "Indexes", "An index is a separate schema object, even when it enforces uniqueness.", + section("Create and remove an index", 'Use an explicit name so the index can be inspected or removed later. Both APIs accept the same Index definition. Fully qualify this type if System.Index is also in scope. Columns retain the order in KeyColumns.
', pair("Index a user name", ''' +Database.AddIndex("Users", new DotNetProjects.Migrator.Framework.Index +{ + Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false +}); +''', ''' +migration.Create.Index("Users", new DotNetProjects.Migrator.Framework.Index +{ + Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false +}); +'''), pair("Drop an index", ''' +Database.RemoveIndex("Users", "IX_Users_Name"); +''', ''' +migration.Delete.Index("IX_Users_Name", "Users"); +''')), + section("Provider options", 'Index definitions also expose IncludeColumns, FilterItems and Clustered. These options are provider-specific. Oracle rejects included and clustered index requests; SQLite reconstruction rejects existing index SQL with explicit COLLATE clauses. Preview handles simple indexes and rejects unsupported options.
'), + section("Unique index or unique constraint?", 'Use UniqueConstraint for a table-level invariant and an Index with Unique for an index definition. Do not infer ownership from a generated name. SQLite RemoveAllIndexes preserves declared table UNIQUE constraints; remove those through the constraint APIs. Check query plans and data cardinality when choosing index keys.
'), source="src/Migrator/Framework/Index.cs") + +page("Schema basics", "constraints", "Keys and constraints", "Declare table invariants independently of column attributes.", + section("Add uniqueness and a check", 'Existing rows must satisfy a new constraint. A rebuild or ALTER operation can fail if duplicate or invalid data is present. CHECK expressions are trusted SQL and depend on the target engine. Primary keys, unique constraints, foreign keys and checks have typed definitions.
', pair("Add two named constraints", ''' +Database.AddUniqueConstraint("UQ_Users_Name", "Users", "Name"); +Database.AddCheckConstraint("CK_Users_Id", "Users", "Id > 0"); +''', ''' +migration.Create.Unique("UQ_Users_Name", "Users", "Name"); +migration.Create.Check("CK_Users_Id", "Users", "Id > 0"); +''')), + section("Remove the intended object", 'Use dedicated primary-key and foreign-key removal methods; generic RemoveConstraint is for unique/check constraints in the SQLite provider. Avoid RemoveAllConstraints unless the migration deliberately replaces every invariant.
', pair("Remove a check constraint", ''' +Database.RemoveConstraint("Users", "CK_Users_Id"); +''', ''' +migration.Delete.Constraint("CK_Users_Id", "Users"); +''')), + section("Constraint identity", 'GetTableConstraints returns ordered typed definitions. SQLite can return a null name for an unnamed legacy constraint; an autoindex name is not a substitute constraint name. PrimaryKeyExists checks the actual key name. MySQL reports the primary key name as PRIMARY.
Altering a column does not give that column ownership of a unique constraint. SQL Server implicit ownership markers are no longer used for deletion. Explicitly remove only the object your migration intends to change.
')) + +page("Schema basics", "foreign-keys", "Foreign keys", "Define ordered child/parent columns and independent actions for update and delete.", + section("Add a relationship", 'Parent key columns must identify a suitable primary/unique key. Child and parent arrays are positional: each child column corresponds to the parent column at the same index. Both tables and their compatible columns must already exist for this example. The Classic example uses IForeignKeyActions for independent update/delete actions; the older AddForeignKey overload supplies one action for both.
', pair("Orders belong to users", ''' +((IForeignKeyActions)Database).AddForeignKey( + "FK_Orders_Users", "Orders", new[] { "UserId" }, + "Users", new[] { "Id" }, ForeignKeyConstraintType.Cascade, + ForeignKeyConstraintType.NoAction); +''', ''' +migration.Create.ForeignKey("FK_Orders_Users", + "Orders", new[] { "UserId" }, "Users", new[] { "Id" }, + onDelete: ForeignKeyConstraintType.Cascade, + onUpdate: ForeignKeyConstraintType.NoAction); +''')), + section("Remove a relationship", 'Remove dependent keys before incompatible table or key changes. Restore them only after the existing data satisfies the replacement relationship.
', pair("Remove the foreign key", ''' +Database.RemoveForeignKey("Orders", "FK_Orders_Users"); +''', ''' +migration.Delete.ForeignKey("FK_Orders_Users", "Orders"); +''')), + section("Database semantics", 'Supported actions depend on the database; do not assume every engine implements CASCADE, RESTRICT, SET NULL and SET DEFAULT identically. SQLite rebuilds preserve separate update/delete actions and validate integrity before an owned transaction commits. MATCH FULL and MATCH PARTIAL requests are rejected because SQLite does not enforce those semantics.
SetNull needs nullable child columns. Test action behavior using actual data, especially composite keys and partially NULL values. Oracle supports its own subset of foreign-key actions.
')) + +page("Schema basics", "defaults-collations", "Defaults and collations", "Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.", + section("Literal and expression defaults", 'Ordinary strings are quoted literal values. RawSql.Insert marks trusted SQL to evaluate on the database. The expression below works on SQLite; provider function names and return types can differ.
', pair("A database-generated timestamp", ''' +Database.AddTable("Events", new Column("CreatedAt", DbType.DateTime) +{ + DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP"), IsNullable = false +}); +''', ''' +migration.Create.Table("Events") + .WithColumn("CreatedAt").OfType(DbType.DateTime).NotNullable() + .WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP")); +''', smoke=True)), + section("Comparison behavior", 'Collation presets request semantics. Unsupported mappings fail before DDL. SQLite AsciiIgnoreCase maps to NOCASE and folds ASCII only; it is not Unicode case folding. Named custom SQLite collations must be registered on the connection before schema or data operations use them.
', pair("ASCII-insensitive SQLite text", ''' +Database.AddTable("Labels", new Column("Name", DbType.String, 100) +{ + Collation = Collation.AsciiIgnoreCase +}); +''', ''' +migration.Create.Table("Labels") + .WithColumn("Name").AsString(100) + .WithCollation(Collation.AsciiIgnoreCase); +''', smoke=True)), + section("Presets and provider names", table(["Request", "Meaning"], [["CaseInsensitive / CaseSensitive", "Case behavior with accent sensitivity; supported SQL Server/MySQL/MariaDB mappings, or an explicit installed name on other engines."], ["Binary", "Provider binary comparison; not a promise of identical linguistic ordering."], ["AsciiIgnoreCase", "SQLite NOCASE; ASCII letters only."], ["Collation.Named(name)", "An installed or registered provider-specific collation."]]), 'Use named collations for language-specific or exact comparison behavior. PostgreSQL ICU nondeterministic collations must be created explicitly; SQL rendering does not create shared database objects. Read the mapping table for engine versions and restrictions.
')) + +page("Migration runners", "runners", "Choose a runner", "Use the same migration assembly in a dedicated host, a DI scope or the command-line tool.", + section("Execution choices", table(["Runner", "A good fit"], [["Library host", "A small deployment executable with explicit connection ownership and full provider access."], ["Microsoft DI integration", "A service collection supplying constructor dependencies, options and logging."], ["migrator CLI", "Automation that selects assemblies, providers, scopes, tags and target versions."]])), + section("A dedicated host", 'Run schema changes before application instances need the new schema. The host below works with either migration style and scans the assembly containing CreateUsers. Use explicit type selection when an assembly also contains migrations for other purposes.
', HOST), + section("Deployment responsibilities", 'Give the deployment identity the schema privileges needed by the selected migrations. Coordinate concurrent deploys through an external orchestrator or supported native lock. Configure the history table and scope consistently across invocations. Log the target and result without exposing connection strings.
Choose the CLI for a ready command surface, or DI for application services. Read transaction and lock semantics before relying on atomicity.
'), source="src/Migrator/Migrator.cs") + +page("Migration runners", "cli", "Command-line tool", "List, validate, plan, preview, apply and reverse migrations from a deployment script.", + section("Install and connect", 'Install DotNetProjects.Migrator.Tool as a .NET tool. Set MIGRATOR_CONNECTION in the deployment environment or select another variable with --connection-env. Both Classic and Fluent classes use the same commands. The tool does not print the connection-string value.
', pair("Install the CLI", 'dotnet tool install --global DotNetProjects.Migrator.Tool', kind="shell")), + section("Inspect before applying", pair("Inspect a migration assembly", ''' +migrator list --assembly MyMigrations.dll --provider SQLite +migrator status --assembly MyMigrations.dll --provider SQLite +migrator validate --assembly MyMigrations.dll --provider SQLite +migrator plan --assembly MyMigrations.dll --provider SQLite --target 10 +migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql +''', kind="shell"), 'Validate checks version planning, not arbitrary migration-body behavior. Plan lists version steps without running bodies. SQL generation renders a supported operation subset; it does not produce an idempotent history-managed bundle.
'), + section("Apply and roll back", pair("Deploy a selected scope", ''' +migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession +migrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0 +''', kind="shell"), 'Rollback requires an explicit lower target and rejects any plan containing upward steps. Target checks run after taking the configured lock and refreshing history. Tags, profiles and provider choice must match the intended deployment.
'), + section("Options and exit codes", table(["Option", "Purpose"], [["--tags a,b / --tag-match Any|All", "Filter versioned migrations."], ["--profiles a,b", "Select named profiles."], ["--schema / --scope", "Provider schema and migration history scope."], ["--timeout SECONDS", "Database command timeout."], ["--lock / --lock-timeout SECONDS", "Native migration lock on supported providers."], ["--offline", "SQL generation assuming empty history; profiles/maintenance rejected."]]), 'Exit codes: 0 success; 1 load/execution failure; 2 invalid arguments; 3 unsupported provider/operation; 4 lock timeout. SQL output can contain data authored in migrations. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird; use a custom host for other library providers.
'), source="src/Migrator.Tool/Program.cs") + +page("Migration runners", "dependency-injection", "Dependency injection and logging", "Resolve the runner and migration dependencies inside one service scope.", + section("Register the integration", 'Install DotNetProjects.Migrator.Extensions.DependencyInjection and Microsoft.Extensions.Logging alongside the core and database driver. This example uses the connection-string provider factory so provider disposal belongs to the DI scope. The providerName explicitly selects the SQLite driver.
', pair("A scoped migration host", ''' +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Providers; +using DotNetProjects.Migrator.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); +services.AddLogging(); +services.AddMigrator(_ => ProviderFactory.Create( + ProviderTypes.SQLite, "Data Source=app.db", defaultSchema: null, + providerName: "Microsoft.Data.Sqlite"), typeof(CreateUsers).Assembly, + options => options.TransactionMode = MigrationTransactionMode.PerMigration); + +using var container = services.BuildServiceProvider(); +using var scope = container.CreateScope(); +scope.ServiceProvider.GetRequiredServiceMigration classes are registered for activation through the service provider. Register your own constructor dependencies before resolving the runner. Options are scoped snapshots; a custom Activator can override construction. Fluent and Classic migrations use the same activation mechanism.
'), + section("Logging boundaries", 'The integration adapts runner lifecycle events to Microsoft logging. It omits SQL text and raw exception messages from these events. Configure your own logging providers through AddLogging. The core retains its lightweight logger API when you do not use DI.
Dispose the scope after migration execution. When supplying a caller-owned open connection, keep its owner alive until after the scope is disposed; the provider does not acquire ownership of an externally supplied connection.
'), source="src/Migrator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs") + +page("Migration runners", "preview", "Planning and SQL preview", "A version plan answers what runs. SQL preview shows the supported operation SQL.", + section("Read-only version planning", 'Plan and DryRun inspect applied versions through IMigrationHistory without creating/upgrading history or invoking migration bodies, callbacks, transactions or SQLite PRAGMA changes. Set the same scope, tags and assembly you intend to deploy. The fragment below assumes an initialized runner.
', pair("Inspect version steps", ''' +var plan = runner.Plan(10); +foreach (var step in plan) + Console.WriteLine($"{step.Version}: {(step.IsUp ? "up" : "down")}"); +runner.DryRun = true; +runner.MigrateTo(10); +''', kind="host")), + section("Preview connected SQL", 'PreviewSql reads connected history and schema. Classic bodies require explicit opt-in; provider calls are captured through a proxy that rejects unsupported access. Fluent authoring builds operations directly. This is trusted C# execution in both cases, not a security sandbox.
', pair("Generate operation SQL", ''' +var sql = runner.PreviewSql(10, ProviderTypes.SQLite, allowLegacyBodies: true); +Console.WriteLine(sql); +''', ''' +var sql = runner.PreviewSql(10, ProviderTypes.SQLite); +Console.WriteLine(sql); +''', kind="host")), + section("Offline generation and boundaries", 'MigrationSqlPreview.Generate can render supported operations without connecting; the CLI exposes --offline. Earlier structured create/rename operations update the planned schema. Raw SQL invalidates that knowledge, so later dependencies can fail.
Basic tables/columns, supported renames, simple indexes, inserts and raw SQL form the preview subset. Unsupported alterations, constraint changes, filters, callbacks and schema dependencies throw. InitializeOnce overrides are rejected rather than skipped silently. Post-commit callbacks do not run. Output contains operation SQL, not history guards or an idempotent deployment bundle.
'), source="src/Migrator/Migrator.cs") + +page("Migration runners", "transactions", "Transactions and locks", "Transaction rollback and cross-process coordination solve different problems.", + section("Choose the transaction boundary", table(["Mode", "Behavior"], [["PerMigration", "Default. Each successful migration commits independently."], ["None", "Provider/operation transaction behavior; no runner-managed migration transaction."], ["WholeSession", "One session transaction on SQLite, PostgreSQL or SQL Server; history initialization happens first."]]), 'Actual atomicity depends on the database and operation. Administration commands or implicit-commit DDL can violate assumptions. AfterUp/AfterDown run after commit; WholeSession defers them until the session commit. A callback failure cannot undo durable changes.
', pair("Configure a session transaction", ''' +runner.Options.TransactionMode = MigrationTransactionMode.WholeSession; +runner.MigrateToLastVersion(); +''', kind="host")), + section("Coordinate competing runners", 'DatabaseMigrationLock uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. The lease is session-owned and remains held across migration commits. This host fragment assumes a supported provider; SQLite rejects this built-in lock.
', pair("Acquire a native deployment lock", ''' +runner.Options.Lock = new DatabaseMigrationLock(); +runner.Options.LockTimeout = TimeSpan.FromSeconds(60); +runner.MigrateToLastVersion(); +''', kind="host")), + section("Scope of protection", 'Native locks are keyed by database, history table and scope. Coordinate separately if different scopes modify shared objects. Do not close/replace the connection, switch databases or manipulate the native lock inside a migration. MySQL named locks coordinate one server, not an entire distributed cluster.
Implement IMigrationLock for another lease mechanism, or serialize deployments outside the process. A transaction, history primary key or ordinary database write lock alone does not prove that the whole migration sequence is serialized.
'), source="src/Migrator/DatabaseMigrationLock.cs") + +page("Migration types", "versioning", "Versioning and scoped history", "Number changes, keep applied source immutable and give independent modules explicit histories.", + section("Choose a version scheme", 'Migration accepts a numeric version or year/month/day/hour/minute/second components. Use one monotonic scheme per migration set. The date constructor builds a numeric identifier; it does not consult a clock or resolve branch collisions for you. Missing lower-numbered versions up to the target can still be applied.
', pair("A dated migration", ''' +[Migration(2026, 9, 23, 10, 0, 0)] +public class AddUserEmail : Migration +{ + public override void Up() => Database.AddColumn("Users", new Column("Email", DbType.String, 320)); + public override void Down() => Database.RemoveColumn("Users", "Email"); +} +''', ''' +[Migration(2026, 9, 23, 10, 0, 0)] +public class AddUserEmail : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Create.Column("Email", "Users").AsString(320); + public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Column("Email", "Users"); +} +''', kind="class")), + section("Scope selection", 'An explicit MigrationAttribute.Scope selects that migration only for the matching provider scope. Unscoped migrations inherit the runner scope. Discovery, duplicate validation and history reads use the effective scope. Duplicate numeric versions in distinct explicit scopes are independent; physical tables are not isolated.
Set SchemaInfoTableName before any history access if you need a different table. AppliedMigrations lists recorded versions; LastAppliedMigrationVersion is nullable when history is empty. AssemblyLastMigrationVersion describes the loaded set.
'), + section("Consolidated baselines", 'A baseline can record versions whose schema it already includes. The runner rechecks active-scope history before each planned step, skipping newly covered versions and their AfterUp callbacks. Downward runs similarly skip versions removed by an earlier Down. Recording the baseline version itself does not create a duplicate.
', pair("Mark a version included by a baseline", ''' +Database.MigrationApplied(1, "billing"); +''', ''' +migration.Execute.WithProvider(provider => provider.MigrationApplied(1, "billing")); +'''), 'Only record a version after establishing the schema it represents. History entries are not a substitute for verifying an existing database. Schema/history rollback follows the selected transaction mode. No migration-content checksum is stored.
'), source="src/Migrator/Migrator.cs") + +page("Migration types", "tags", "Tags", "Select a subset of versioned migrations using explicit ordinal names.", + section("Tag migration classes", 'Tags is in DotNetProjects.Migrator. One class can declare multiple names. Choose names for deployment intent such as core or reporting; do not use a tag to hide a dependency that a selected migration still requires.
', pair("A reporting migration", ''' +[Migration(2), Tags("reporting")] +public class CreateReportLog : Migration +{ + public override void Up() => Database.AddTable("ReportLog", new Column("Name", DbType.String, 255)); + public override void Down() => Database.RemoveTable("ReportLog"); +} +''', ''' +[Migration(2), Tags("reporting")] +public class CreateReportLog : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Create.Table("ReportLog").WithColumn("Name").AsString(255); + public override void BuildDown(MigrationBuilder migration) + => migration.Delete.Table("ReportLog"); +} +''', kind="class", smoke=True)), + section("Configure matching", pair("Select tags on the runner", ''' +runner.Options.Tags.Add("reporting"); +runner.Options.TagMatch = TagMatchMode.Any; +runner.MigrateToLastVersion(); +''', kind="host"), 'Any requires at least one selected tag; All requires every selected tag. Matching is ordinal and case-sensitive. Without a tag filter all eligible versioned migrations are selected. Profiles have their own explicit name selection.
'), + section("Downgrade behavior", 'Applied versions excluded by the active filter remain applied during downgrade. A filtered run is therefore not a promise that the whole database matches one contiguous global version range. Keep deployment filters stable and inspect the plan before reversing selected changes.
'), source="src/Migrator/MigrationLoader.cs") + +page("Migration types", "profiles", "Profiles", "Run explicitly selected work after versioned migrations without recording a version.", + section("Define a named profile", 'A profile is useful for optional seed data or environment setup. It runs every time its name is selected. Make repeated execution deliberate: use an identifying predicate or other idempotent operation where appropriate.
', pair("A development seed profile", ''' +[Profile("demo", Order = 10)] +public class DemoData : Migration +{ + public override void Up() => Database.InsertIfNotExists("Users", + new[] { "Id", "Name" }, new object[] { 1, "Ada" }, + new[] { "Id" }, new object[] { 1 }); + public override void Down() { } +} +''', ''' +[Profile("demo", Order = 10)] +public class DemoData : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Insert.IntoTable("Users") + .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" }) + .IfNotExists(new[] { "Id" }, new object[] { 1 }); + public override void BuildDown(MigrationBuilder migration) { } +} +''', kind="class")), + section("Select a profile", pair("Run the demo profile", ''' +runner.Options.Profiles.Add("demo"); +runner.MigrateToLastVersion(); +''', kind="host"), 'Profiles accept Order and Scope. Execution orders by Order and then ordinal full type name. Profile execution uses Up and does not create a migration-version entry or use Down as an undo history. An auxiliary-only run preserves existing version history.
'), + section("Execution versus repeatables", 'A selected profile runs because it was selected, not because its source checksum changed. Treat this separately from versioned migrations and checksum-based repeatable SQL. Offline CLI SQL generation rejects profiles because it cannot represent the complete lifecycle.
'), source="src/Migrator/RunnerOptions.cs") + +page("Migration types", "maintenance", "Maintenance migrations", "Place ordered work at the runner's lifecycle stages.", + section("Choose a stage", table(["Stage", "When"], [["BeforeRun", "Before versioned migration work in this run."], ["BeforeMigration", "Before each executed versioned migration."], ["AfterMigration", "After each executed versioned migration."], ["AfterRun", "After the run's migration/profile work."]]), 'Maintenance classes accept Order and Scope. They use Up and do not acquire version records. Hooks stop on failure; later stages are not finally blocks or guaranteed cleanup paths. Lock release and connection/transaction restoration are runner responsibilities.
'), + section("A scoped maintenance operation", 'The example expects an existing DeploymentLog table. Choose a table that already exists at the selected stage. Fluent callbacks execute at the corresponding operation position.
', pair("Write a deployment marker", ''' +[Maintenance(MaintenanceStage.AfterRun, Order = 10)] +public class RecordDeployment : Migration +{ + public override void Up() + => Database.Insert("DeploymentLog", new[] { "Message" }, new object[] { "Migration run finished" }); + public override void Down() { } +} +''', ''' +[Maintenance(MaintenanceStage.AfterRun, Order = 10)] +public class RecordDeployment : FluentMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Insert.IntoTable("DeploymentLog") + .Row(new[] { "Message" }, new object[] { "Migration run finished" }); + public override void BuildDown(MigrationBuilder migration) { } +} +''', kind="class")), + section("Post-commit callbacks", 'Migration.AfterUp and AfterDown run after commit, with the migration context restored. In WholeSession they wait until the entire session commits. Their failure reports an error after durable changes; it cannot reverse that commit. Do not confuse maintenance stages with a guaranteed post-commit delivery system.
'), source="src/Migrator/Migrator.cs") + +page("Migration types", "auto-reversing", "Automatic reversal", "Fluent operations can describe a supported reverse sequence; Classic migrations author it directly.", + section("Creation and its reverse", 'AutoReversingMigration derives reverse operations in reverse order and validates reversal support before its first change. The Classic equivalent makes the Down operation explicit. Both examples below create the same table and remove it on downgrade.
', pair("A reversible table creation", ''' +[Migration(3)] +public class CreateNotes : Migration +{ + public override void Up() => Database.AddTable("Notes", new Column("Text", DbType.String, 500)); + public override void Down() => Database.RemoveTable("Notes"); +} +''', ''' +[Migration(3)] +public class CreateNotes : AutoReversingMigration +{ + public override void BuildUp(MigrationBuilder migration) + => migration.Create.Table("Notes").WithColumn("Text").AsString(500); +} +''', kind="class", smoke=True)), + section("What needs an authored reverse", 'Destructive changes, data operations, SQL and callbacks require explicit reverse behavior. Reverse support is narrower than execution support. An operation that can run is not necessarily one that can be inverted from its definition alone.
Use FluentMigration with BuildDown when the reverse needs its own steps. MigrationBuilder.WithReverse can attach an explicit backward operation to a forward operation. Dropping a newly created table on downgrade still destroys any data inserted since creation; automatic reversal is not data recovery.
'), source="src/Migrator/Framework/Fluent/FluentMigration.cs") + +page("Database providers", "providers", "Provider overview", "One authoring contract, explicit database behavior. Choose the driver and provider together.", + section("Database families", table(["Database", "ProviderTypes", "Guide"], [["SQLite", "SQLite / MonoSQLite", 'Live-schema reconstruction'], ["SQL Server", "SqlServer / SqlServer2005", 'Constraints, batches and locks'], ["PostgreSQL", "PostgreSQL / PostgreSQL82", 'Schemas, types and locks'], ["MySQL / MariaDB", "Mysql / MariaDB", 'DDL and collation behavior'], ["Oracle", "Oracle / MsOracle", 'Identity and metadata'], ["SAP HANA", "Hana", 'Additional providers'], ["Db2 / Informix / Firebird / Ingres / Sybase", "IBM_DB2 / IBM_Informix / Firebird / Ingres / Sybase", 'Engine-specific guidance']])), + section("Bring a connection", 'Pass an open IDbConnection to ProviderFactory.Create. Both migration styles use that provider. Alternatively use the connection-string overload and configure providerName so the provider can resolve the ADO.NET factory. Use a matching driver and test the exact server version you deploy.
', pair("Provider selection · open connection supplied by the host", ''' +using var selectedProvider = ProviderFactory.Create( + ProviderTypes.PostgreSQL, connection, defaultSchema: "public", scope: "billing"); +var selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false); +selectedRunner.MigrateToLastVersion(); +''', kind="host")), + section("Qualification", 'The CI matrix includes SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase and SAP HANA. Ingres and historical provider aliases have separate qualification needs. Read testing and the live-engine matrix for exact drivers and server setup.
Database support is operation-specific. Column types, collation presets, index options, DDL transactions and metadata readers can differ. Test stored values and preserved schema, not only the generated SQL.
'), source="src/Migrator/ProviderFactory.cs") + +SQLITE_ALTER = pair("Change a column on an existing SQLite table", ''' +Database.ChangeColumn("Users", new Column("Name", DbType.String, 500) +{ + IsNullable = false, DefaultValue = "Unknown", + Collation = Collation.AsciiIgnoreCase +}); +''', ''' +migration.Alter.Column("Name", "Users") + .AsString(500).NotNullable().WithDefaultValue("Unknown") + .WithCollation(Collation.AsciiIgnoreCase); +''') + +page("Database providers", "sqlite", "SQLite", "Change existing tables from the live schema, without maintaining an ORM model.", + section("Automatic reconstruction", 'For supported changes, Migrator reads SQLiteTableInfo, changes its representation, creates a replacement table, copies mapped rows, swaps tables and recreates represented dependent objects. This provides column type/default/nullability changes and adding/removing primary, foreign, unique and check constraints.
Native rename and eligible drop-column paths are used when supported by the engine. Complex alterations use reconstruction. Existing rows must satisfy the new definition; a default does not rewrite every existing NULL during a column change.
', SQLITE_ALTER), + section("What survives a rebuild", table(["Detail", "Behavior"], [["Mapped data", "Named-column copy preserves mapped values, subject to the new definition accepting them."], ["Keys and constraints", "Named/composite keys, ordered foreign-key pairs and separate update/delete actions are retained."], ["Column collations", "Declared names are retained. Register custom collations on the connection."], ["Indexes and triggers", "Supported definitions are recreated; unsafe trigger rename/drop-column cases are rejected."], ["AUTOINCREMENT", "The sequence high-water mark survives, including previously deleted identities."], ["Hidden rowid", "Not part of the mapped data and may change."]])), + section("Boundaries are explicit", 'Reconstruction rejects generated columns, STRICT, WITHOUT ROWID and indexes with explicit COLLATE clauses. It is not an arbitrary SQL dependency rewriter. Adjust dependent views, complex expressions and triggers explicitly when required. MATCH FULL and MATCH PARTIAL are rejected because SQLite does not enforce their semantics.
Owned rebuild transactions and runner transactions validate foreign-key integrity before commit and restore the prior enforcement setting. For caller-owned active transactions configure foreign keys before beginning the transaction. A SQLite write lock is not a session-wide migration lease; coordinate deployment externally or provide IMigrationLock.
'), + section("Values and identity", 'CLR Guid defaults use blobs from Guid.ToByteArray(), matching inserted parameters. Legacy text GUID defaults remain SQL expressions during unrelated rebuilds, so storage is not silently converted. Convert mixed text/blob keys explicitly and consistently across related tables.
SQLite INTEGER is signed 64-bit. Declared text lengths and decimal precision do not impose SQL Server-like enforcement. An identity needs a single INTEGER primary key in the same definition. For adding identity to an existing table, use an atomic SQLite RecreateTable definition containing both objects.
'), + section("How this differs from other tools", 'FluentMigrator leaves general column alterations and later foreign-key changes to manual reconstruction. DbUp and Evolve run supplied scripts. EF Core also rebuilds SQLite tables using model-represented artifacts. Migrator reconstructs from live metadata without an ORM. The sourced operation comparison distinguishes native SQL, emulation and manual work.
'), source="src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs") + +page("Database providers", "sql-server", "SQL Server", "Explicit keys, provider-specific indexes, transactional DDL and application locks.", + section("Select the provider", 'Use ProviderTypes.SqlServer with an open Microsoft.Data.SqlClient connection. Pass the intended default schema, commonly dbo. Historical SqlServer2005 is a separate alias with older type mappings. WholeSession transactions and DatabaseMigrationLock are available for SQL Server.
'), + section("Name constraints explicitly", 'Column changes preserve explicit constraints and indexes. Add/remove uniqueness independently. For a nonclustered primary key on an existing compatible table use the dedicated API shown below. Review existing clustered indexes before changing key layout.
', pair("Add a nonclustered primary key", ''' +Database.AddPrimaryKeyNonClustered("PK_Users", "Users", "Id"); +''', ''' +migration.Create.NonClusteredPrimaryKey("PK_Users", "Users", "Id"); +''')), + section("Indexes and SQL batches", 'Index definitions can express included/filter/cluster options where supported. The script APIs split standalone GO lines; raw ExecuteNonQuery/Execute.Sql does not. SQLCMD directives and GO repetition are rejected before executing script batches. Prefer scripts for client batch syntax and commands for parameterized statements.
'), + section("Types and object names", 'Use TimeOnly for time values and TimeSpan for interval ticks. SqlServer2005 uses its older DATETIME precision behavior. Use separate quoting helpers for table and column names. A table rename leaves named constraints/indexes attached with their old names; assign distinct names when creating a replacement table.
'), source="src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs") + +page("Database providers", "postgresql", "PostgreSQL", "Native intervals, schema-aware metadata, transactional DDL and advisory locks.", + section("Connect with Npgsql", 'Use ProviderTypes.PostgreSQL and an open Npgsql connection, with the intended default schema. Connection search_path affects unqualified relation lookup. Metadata readers resolve the requested relation through PostgreSQL and distinguish same-named tables in different schemas.
'), + section("Use native interval values", 'PostgreSQL maps duration values to native intervals. Time without time zone maps to a time of day; use TimeOnly for that input. Parameter mappings and scalar CLR return types are separate concerns: raw ADO.NET values remain driver-specific.
', pair("Store a job duration", ''' +Database.AddColumn("Jobs", new Column("Elapsed", MigratorDbType.Interval) +{ + DefaultValue = TimeSpan.FromDays(2) +}); +''', ''' +migration.Create.Column("Elapsed", "Jobs").OfType(MigratorDbType.Interval) + .WithDefaultValue(TimeSpan.FromDays(2)); +''')), + section("Collations and schemas", 'Create any ICU nondeterministic collation explicitly, then select it with Collation.Named. Column rendering does not silently create shared collation objects. Binary maps to C; language and case semantics should use a specific installed name.
Schema-aware metadata does not establish complete qualification for every operation. Test quoted names and search-path behavior with your migration. Renaming a table retains its named constraints; avoid colliding names when recreating the old table.
'), + section("Transactions and coordination", 'WholeSession is supported for verified transactional DDL, and DatabaseMigrationLock uses a session advisory lock. Statements that require special transaction treatment need a separate deployment design. Keep the connection stable while the lease is held.
'), source="src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs") + +page("Database providers", "mysql", "MySQL and MariaDB", "Related providers with explicit engine, collation and DDL transaction differences.", + section("Choose the matching dialect", 'Select ProviderTypes.Mysql for MySQL and MariaDB for MariaDB. Use an open driver connection or configure the factory. Do not treat compatible wire protocols as proof of identical server syntax or metadata behavior. DDL can commit implicitly; the runner rejects WholeSession for these dialects.
'), + section("Select a supported collation", 'Semantic presets require utf8mb4-compatible text and the documented server versions: MySQL 8 and MariaDB 10.10+ have different mappings. Use a named installed collation if exact linguistic or trailing-space behavior matters.
', pair("Case-insensitive, accent-sensitive text", ''' +Database.AddTable("Labels", new Column("Name", DbType.String, 100) +{ + Collation = Collation.CaseInsensitive +}); +''', ''' +migration.Create.Table("Labels").WithColumn("Name").AsString(100) + .WithCollation(Collation.CaseInsensitive); +''')), + section("Constraint metadata", 'MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.
'), + section("Locking and values", 'DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.
'), source="src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs") + +page("Database providers", "oracle", "Oracle", "Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.", + section("Connection and schema", 'Use ProviderTypes.Oracle with the Oracle managed ADO.NET driver and the intended schema. MsOracle is a historical variant. Oracle DDL is not generally atomic across a migration; WholeSession is rejected. Some quoted qualified metadata lookups are explicitly rejected.
'), + section("Create an identity definition", 'Identity is a column attribute and is validated before table creation. It need not be a primary key on every engine, but the example pairs it with an explicit key. Use a server/driver combination qualified for native identity.
', pair("An identity table with an explicit key", ''' +Database.AddTable("Entries", + new Column("Id", DbType.Int32) { IsIdentity = true, IsNullable = false }, + new Column("Text", DbType.String, 255), + new PrimaryKeyConstraint("PK_Entries", "Id")); +''', ''' +migration.Create.Table("Entries") + .WithColumn("Id").AsInt32().Identity().NotNullable() + .WithColumn("Text").AsString(255) + .WithPrimaryKey("PK_Entries", "Id"); +''', smoke=True)), + section("Object cleanup", 'RemoveTable leaves unrelated sequences intact. Oracle removes table-owned triggers and native identity objects. For a legacy sequence that the migration explicitly owns, OracleTransformationProvider.RemoveTableWithOwnedSequences validates named sequences and propagates cleanup errors. It does not infer sequence ownership from naming patterns.
'), + section("Values and options", 'Oracle empty character strings become NULL. Time uses DATE with a fixed 1970-01-01 date and whole-second precision; fractional Time inputs are rejected. Intervals use native storage. Changes that require an unsupported in-place type conversion need an explicit data migration.
Included/clustered index options are rejected rather than ignored. Ordered foreign-key pairs and delete actions are preserved by structured metadata. A SQL Server clustered-index request is not translated into an Oracle index-organized table.
'), source="src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs") + +page("Database providers", "other-providers", "HANA and additional providers", "Use the live-engine matrix to qualify operations beyond the common database families.", + section("SAP HANA", 'ProviderTypes.Hana uses SAP’s native .NET driver. The CI job runs HANA Express and exercises schema/data operations, metadata, constraints, history, restart and DML rollback. DDL may autocommit. Use a custom host; the CLI driver bundle does not include an online HANA host.
HANA has its own supported type set; Guid and DateTimeOffset are outside the current matrix mappings. Review the type matrix before choosing shared column definitions.
'), + section("Db2, Informix, Firebird and Sybase", table(["Provider", "Things to check"], [["Db2 LUW", "Driver runtime dependencies, decimal/storage capacity and ordinary/unique index options."], ["Informix", "Native driver and database encoding; TEXT reads, integer NULL sentinels, whole-second Time and trailing-space trimming."], ["Firebird", "Decimal storage capacity, ordinary/unique index operations and transaction behavior."], ["Sybase ASE", "TEXTSIZE and string truncation settings, nullable BIT restrictions, trimmed strings and constraint-name limitations."]])), + section("A shared table definition", 'The same authoring API describes a portable subset. This does not imply that every native extension or type maps identically. Start with simple definitions, then qualify your actual data and schema operations on each target.
', CREATE_USERS), + section("Source inventory and evidence", 'Ingres remains a source dialect outside the eleven-engine matrix. Redshift, Snowflake and Db2 for IBM i require separate provider/infrastructure qualification; PostgreSQL tests do not qualify Redshift, and Db2 LUW tests do not qualify IBM i.
See qualification requirements and live test setup for exact coverage and reproduction commands.
'), source="src/Migrator/ProviderFactory.cs") + +page("Advanced topics", "conditional", "Conditional logic", "Choose between inspecting the live schema and declaring a provider-specific operation.", + section("Provider-specific operations", 'The Classic provider indexer selects a named provider or a no-op provider. Fluent IfDatabase wraps structured operations in a provider condition. Use the provider names understood by the dialect; this SQLite example leaves other providers unchanged.
', pair("Run a SQLite-specific statement", ''' +Database["SQLite"].ExecuteNonQuery("UPDATE Users SET Name = upper(Name)"); +''', ''' +migration.IfDatabase("SQLite", sqlite => + sqlite.Execute.Sql("UPDATE Users SET Name = upper(Name)")); +''')), + section("Schema-dependent decisions", 'Use Database.TableExists/ColumnExists or FluentMigration.Schema for connected checks. These inspect the current database. A fluent BuildUp method collects operations before they execute, so queued creation is not visible to a live metadata read in the same method.
For execution-time decisions after earlier operations, use an explicit provider callback. That callback cannot be previewed and requires an authored reverse. Avoid making a migration silently succeed with the wrong schema: an existence check alone does not validate a column’s type or constraint definition.
'), source="src/Migrator/Framework/Fluent/MigrationBuilder.cs") + +page("Advanced topics", "extensions", "Custom extensions", "Reuse schema conventions without hiding provider behavior or changing the migration contract.", + section("Share a schema convention", 'A small helper can express a repeated column policy in both styles. Keep helper behavior stable for historical migrations; changing a helper can change what an old migration does on a fresh database. The example uses static methods to keep its dependencies explicit.
', pair("Reusable audit-column helpers", ''' +public static class AuditColumns +{ + public static void Add(ITransformationProvider database, string table) + => database.AddColumn(table, new Column("CreatedAt", DbType.DateTime) + { + IsNullable = false, DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP") + }); +} +''', ''' +public static class AuditColumns +{ + public static void Add(MigrationBuilder migration, string table) + => migration.Create.Column("CreatedAt", table).OfType(DbType.DateTime) + .NotNullable().WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP")); +} +''', kind="class")), + section("Custom activation and locks", 'RunnerOptions.Activator constructs migrations when a DI container is not appropriate. IMigrationLock supplies a disposable lease for custom deployment coordination. Release must work on success and failure. Configure these at the host boundary rather than in individual migrations.
'), + section("Provider authors", 'ITransformationProvider defines execution and metadata operations. A custom provider needs accurate typed constraint definitions or an explicit unsupported error. IMigrationHistory enables read-only planning and effective-scope history selection. IScriptBatchProvider extends script processing.
Keep SQL rendering independent of a live connection. Custom MigrationOperation implementations need deliberate validation, application, SQL rendering and reversal behavior. See the API map and implementation contracts before claiming preview or reversal support.
'), source="src/Migrator/Framework/Fluent/Operations.cs") + +page("Advanced topics", "testing", "Testing and deployment", "Verify stored data, preserved schema and repeat execution on the actual target engine.", + section("Test a migration lifecycle", 'Create a disposable database, apply the migration, check the schema and rows, run to the same target again, then downgrade and verify the intended reverse. Both authoring styles use the same runner. This fragment assumes an initialized runner whose migration set creates Users.
', pair("A host-level smoke check", ''' +runner.MigrateToLastVersion(); +if (!provider.TableExists("Users")) throw new Exception("Users missing"); +runner.MigrateToLastVersion(); +runner.MigrateTo(0); +if (provider.TableExists("Users")) throw new Exception("Users was not removed"); +''', kind="host")), + section("What to assert", 'Test defaults by omitting a value, and nullability by explicitly sending NULL. Verify composite-key order, foreign-key actions and constraint names. After a SQLite rebuild check real rows, collations, supported indexes/triggers and identity high-water state. Test failure paths as well as successful SQL generation.
Use representative production-sized data to measure lock duration and backfill cost. A passing SQL-string assertion does not establish that a database accepts a command or preserves its semantics.
'), + section("Repository checks", 'Build with dotnet build Migrator.slnx, then use .github/scripts/test.ps1 -Database Unit or SQLite for local suites. The live-engine guide gives the external database setup. Homepage CI results include commit provenance and skipped/missing-suite status.
'), + section("Production rollout", 'Keep applied migrations immutable, review SQL and explicit reverse behavior, and serialize competing deploys. Validate against a restored database before making a breaking change. Plan application compatibility around expand/backfill/contract phases. Treat post-commit callback failures as durable migrations requiring follow-up handling.
'), source=".github/workflows/dotnetpull.yml") + +page("Advanced topics", "upgrading", "Upgrading existing migrations", "Update source definitions while preserving the history your databases already contain.", + section("Explicit column attributes", 'Replace old ColumnProperty flags with IsNullable, IsIdentity and IsUnsigned. Primary, unique, foreign and check constraints belong to the table. GetColumns returns column attributes; use GetTableConstraints for key membership and ordered columns.
Keep the same applied migration versions and effective scope when recompiling. Do not create a new history table merely to make an incompatible source assembly run. Verify the upgrade against a restored database and a fresh database.
'), + section("One fluent authoring surface", 'FluentMigration.BuildUp/BuildDown replaces the duplicate legacy builder. Use complete table definitions for keys, explicit operations for indexes, and independent foreign-key update/delete actions. Classic Up/Down migrations remain first-class.
', CREATE_USERS), + section("Behavior changes to review", 'Column changes preserve explicit uniqueness; old SQL Server ownership markers no longer control deletion. TimeSpan inputs mean intervals, so convert clock-time inputs to TimeOnly. SQLite GUID defaults use the same blob representation as inserted parameters; unrelated rebuilds preserve existing text defaults.
Read the complete compatibility migration guide for constructor replacements, custom-provider contracts, identity, constraint metadata and collation mappings. Version-specific details live there; these chapters describe the current API.
'), source="docs/migration-guide-12.1-to-13.md") + +page("Reference", "api-map", "Classic / Fluent API map", "A practical index of the two authoring surfaces and their shared provider contracts.", + section("Schema operations", table(["Classic", "Fluent"], [["AddTable", "Create.Table"], ["AddColumn(table, column)", "Create.Column(name, table)"], ["ChangeColumn(table, column)", "Alter.Column(name, table) / Alter.Column(table, column)"], ["RemoveTable / RemoveColumn", "Delete.Table / Delete.Column"], ["RenameTable / RenameColumn", "Rename.Table / Rename.Column"], ["AddPrimaryKey / AddUniqueConstraint / AddCheckConstraint", "Create.PrimaryKey / Create.Unique / Create.Check"], ["AddForeignKey / RemoveForeignKey", "Create.ForeignKey / Delete.ForeignKey"], ["AddIndex / RemoveIndex", "Create.Index / Delete.Index"], ["GetTableConstraints / GetColumns", "Schema.Table(name).ConstraintDefinitions() / Columns()"]])), + section("Data and execution", table(["Classic", "Fluent"], [["Insert / InsertIfNotExists", "Insert.IntoTable(...).Row(...) / IfNotExists(...)"], ["Update / Delete", "Update.Table(...).Set(...).Where(...) / Delete.FromTable(...).Where(...)"], ["ExecuteNonQuery / ExecuteScript / ExecuteResourceScript", "Execute.Sql / Execute.Script / Execute.EmbeddedScript"], ["CopyDataFromTableToTable / UpdateTargetFromSource", "Execute.CopyData / Execute.UpdateFrom"], ["TruncateTable", "Execute.Truncate"], ["CreateCommand / Connection", "Execute.WithCommand / Execute.WithConnection"], ["Database provider access", "Context or Execute.WithProvider"], ["History / transactions", "Shared runner and explicit provider context"]])), + section("Execution is not preview or reversal", 'Both APIs reach the same provider layer, but not every operation has SQL-preview or automatic-reversal support. Provider capabilities still govern execution. Read preview, reversal and the machine-checked method-family inventory for the distinction.
'), source="src/Migrator/Framework/Fluent/MigrationBuilder.cs") + +page("Reference", "contributing", "Contributing", "Make a provider change reproducible, then verify its observable behavior.", + section("A useful report", 'Include package, database and driver versions, a minimal migration, relevant schema/data, and expected versus actual behavior. Remove secrets from connection strings and logs. File reports in the issue tracker.
'), + section("A focused change", 'Add a regression that fails before the fix and checks the real result afterward. Provider-specific changes need actual-engine evidence; skipped tests and generated SQL alone do not qualify support. Keep mutable definition inputs independent from caller arrays and check failure paths.
'), + section("Documentation changes", 'Edit docs/_src/content.py for chapters and docs/_src/home.html for the homepage. Run python .github/scripts/build-docs.py to regenerate static HTML and the search index. Run python .github/scripts/verify-docs.py --compile to validate links, paired examples and compilable C# samples. Site assets live in docs/assets.
Each migration-operation example should provide Classic and Fluent versions. Shared runner and shell commands intentionally appear in both tabs. Keep provider restrictions precise and features described in the present tense. Run the homepage CI-count renderer tests when changing the site template.
'), + section("Design references", 'The chapter organization follows the learning path of FluentMigrator’s documentation, adapted to this API. Visual references include Resend’s typography and code tabs and Gel’s code walkthroughs. The site uses its own palette, layout, copy and migration illustrations.
'), source="docs/_src/content.py") diff --git a/docs/_src/home.html b/docs/_src/home.html new file mode 100644 index 00000000..f8897003 --- /dev/null +++ b/docs/_src/home.html @@ -0,0 +1,34 @@ +{{header}} +DOTNETPROJECTS / DATABASE MIGRATIONS
+A table today. A different table tomorrow. Keep every change explicit, versioned, and close to your application.
+Choose Classic or Fluent migrations. Bring your ADO.NET driver. Run the same migration system alongside any ORM—or without one.
+ + +01 / AUTHORWrite a numbered change.
02 / REVIEWPlan the next step.
03 / APPLYLeave a lasting record.
SMALL PRIMITIVES. REAL DATABASES.
Direct provider calls or a fluent builder. Tables, columns, indexes, constraints and data, with raw SQL when you need it.
Compare the APIs ↗Version plans, tags, profiles, maintenance stages, transaction modes and native locks. A CLI or a runner inside your own host.
Choose a runner ↗Track applied versions and give modules separate histories through scopes. Author the reverse for changes that need it.
Understand versioning ↗A PARTICULAR STRENGTH / SQLITE
Change the schema you have. Without an ORM model.
Migrator reads SQLite’s live schema and automatically reconstructs tables for supported changes to column types, defaults and nullability, and primary, foreign, unique and check constraints.
Existing rows are copied and supported schema artifacts are preserved. You describe the change; the provider handles the rebuild.
FluentMigrator requires manual reconstruction for general column alterations and later foreign-key changes. DbUp and Evolve leave it to your scripts. EF Core also rebuilds tables, using model metadata.
Read the SQLite guide and preservation limits ↗IdINTEGER · PRIMARY KEY
Name255 500 · NOT NULL
↳ Existing rows travel with the schema.
FROM EMPTY FOLDER TO FIRST TABLE
Install the library and your database driver. Add the migration above, wire up the runner, and apply it. SQLite makes a convenient first database.
Follow the complete quick start ↗THE MIGRATION MANUAL
Detailed guides, paired examples, and provider notes.
Browse every chapter ↗
Tables, columns, data, indexes and constraints.
02 / EXECUTIONConfiguration, transactions, DI, CLI and SQL preview.
03 / DATABASESStorage behavior, SQLite rebuilds and engine differences.
04 / PRACTICEReversals, conditional changes, testing and upgrades.
BRING YOUR DATABASE
Capabilities follow the database engine. The provider guide explains aliases, drivers and operation-specific behavior.
BUILD AND DATABASE TESTS
The CI matrix runs unit tests and 11 database suites, then checks for missing or duplicate test assignments. Counts represent test executions across the matrix; skipped tests are shown separately.
+Test totals are populated during deployment from the latest completed master CI run. View CI runs and test-result artifacts ↗.
+EXPLICIT CHANGES. A LASTING RECORD.
Rename objects and evolve populated tables while preserving the schema details you still need.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Use explicit old and new names. The column rename signature is table, old name, new name in both APIs. A table rename does not rename explicit constraints or their backing indexes. Reusing the original key name for a replacement table may collide on SQL Server or PostgreSQL.
Database.RenameTable("Users", "Members");
+Database.RenameColumn("Members", "Name", "DisplayName");migration.Rename.Table("Users", "Members");
+migration.Rename.Column("Members", "Name", "DisplayName");Inside Up() / BuildUp(MigrationBuilder migration)
Supply the type, length, nullability, default and collation you intend to retain. ChangeColumn replaces the column definition; it does not infer that table constraints should be created or removed. Existing rows must remain valid for the new definition.
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
+{
+ IsNullable = false
+});migration.Alter.Column("Name", "Users")
+ .AsString(500).NotNullable();Inside Up() / BuildUp(MigrationBuilder migration)
Add a nullable column, deploy code that can read both forms, backfill values, then enforce the final requirement in a later migration. Large data copies and index creation can hold locks for substantial time; test them against a representative dataset.
On SQLite, a supported alteration may recreate the table and copy rows. On Oracle and some other engines, DDL may commit implicitly. Review the transaction guide and your provider page before choosing the deployment boundary.
A practical index of the two authoring surfaces and their shared provider contracts.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
| Classic | Fluent |
|---|---|
| AddTable | Create.Table |
| AddColumn(table, column) | Create.Column(name, table) |
| ChangeColumn(table, column) | Alter.Column(name, table) / Alter.Column(table, column) |
| RemoveTable / RemoveColumn | Delete.Table / Delete.Column |
| RenameTable / RenameColumn | Rename.Table / Rename.Column |
| AddPrimaryKey / AddUniqueConstraint / AddCheckConstraint | Create.PrimaryKey / Create.Unique / Create.Check |
| AddForeignKey / RemoveForeignKey | Create.ForeignKey / Delete.ForeignKey |
| AddIndex / RemoveIndex | Create.Index / Delete.Index |
| GetTableConstraints / GetColumns | Schema.Table(name).ConstraintDefinitions() / Columns() |
| Classic | Fluent |
|---|---|
| Insert / InsertIfNotExists | Insert.IntoTable(...).Row(...) / IfNotExists(...) |
| Update / Delete | Update.Table(...).Set(...).Where(...) / Delete.FromTable(...).Where(...) |
| ExecuteNonQuery / ExecuteScript / ExecuteResourceScript | Execute.Sql / Execute.Script / Execute.EmbeddedScript |
| CopyDataFromTableToTable / UpdateTargetFromSource | Execute.CopyData / Execute.UpdateFrom |
| TruncateTable | Execute.Truncate |
| CreateCommand / Connection | Execute.WithCommand / Execute.WithConnection |
| Database provider access | Context or Execute.WithProvider |
| History / transactions | Shared runner and explicit provider context |
Both APIs reach the same provider layer, but not every operation has SQL-preview or automatic-reversal support. Provider capabilities still govern execution. Read preview, reversal and the machine-checked method-family inventory for the distinction.
Fluent operations can describe a supported reverse sequence; Classic migrations author it directly.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
AutoReversingMigration derives reverse operations in reverse order and validates reversal support before its first change. The Classic equivalent makes the Down operation explicit. Both examples below create the same table and remove it on downgrade.
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(3)]
+public class CreateNotes : Migration
+{
+ public override void Up() => Database.AddTable("Notes", new Column("Text", DbType.String, 500));
+ public override void Down() => Database.RemoveTable("Notes");
+}using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(3)]
+public class CreateNotes : AutoReversingMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ => migration.Create.Table("Notes").WithColumn("Text").AsString(500);
+}Choose one authoring style
List, validate, plan, preview, apply and reverse migrations from a deployment script.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Install DotNetProjects.Migrator.Tool as a .NET tool. Set MIGRATOR_CONNECTION in the deployment environment or select another variable with --connection-env. Both Classic and Fluent classes use the same commands. The tool does not print the connection-string value.
dotnet tool install --global DotNetProjects.Migrator.Tooldotnet tool install --global DotNetProjects.Migrator.ToolShared commands · both styles
migrator list --assembly MyMigrations.dll --provider SQLite
+migrator status --assembly MyMigrations.dll --provider SQLite
+migrator validate --assembly MyMigrations.dll --provider SQLite
+migrator plan --assembly MyMigrations.dll --provider SQLite --target 10
+migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sqlmigrator list --assembly MyMigrations.dll --provider SQLite
+migrator status --assembly MyMigrations.dll --provider SQLite
+migrator validate --assembly MyMigrations.dll --provider SQLite
+migrator plan --assembly MyMigrations.dll --provider SQLite --target 10
+migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sqlShared commands · both styles
Validate checks version planning, not arbitrary migration-body behavior. Plan lists version steps without running bodies. SQL generation renders a supported operation subset; it does not produce an idempotent history-managed bundle.
migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession
+migrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession
+migrator rollback --assembly MyMigrations.dll --provider SQLite --scope billing --target 0Shared commands · both styles
Rollback requires an explicit lower target and rejects any plan containing upward steps. Target checks run after taking the configured lock and refreshing history. Tags, profiles and provider choice must match the intended deployment.
| Option | Purpose |
|---|---|
| --tags a,b / --tag-match Any|All | Filter versioned migrations. |
| --profiles a,b | Select named profiles. |
| --schema / --scope | Provider schema and migration history scope. |
| --timeout SECONDS | Database command timeout. |
| --lock / --lock-timeout SECONDS | Native migration lock on supported providers. |
| --offline | SQL generation assuming empty history; profiles/maintenance rejected. |
Exit codes: 0 success; 1 load/execution failure; 2 invalid arguments; 3 unsupported provider/operation; 4 lock timeout. SQL output can contain data authored in migrations. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird; use a custom host for other library providers.
Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Column builders take column name followed by table name. Classic AddColumn takes table name first. New nullable columns accept existing rows without a backfill. A required column usually needs a compatible default or a staged data migration.
Database.AddColumn("Users", new Column("Email", DbType.String, 320));migration.Create.Column("Email", "Users").AsString(320).Nullable();Inside Up() / BuildUp(MigrationBuilder migration)
Database.RemoveColumn("Users", "Email");migration.Delete.Column("Email", "Users");Inside Up() / BuildUp(MigrationBuilder migration)
For decimal values specify precision and scale. In a Column constructor an integer after the type is the size, not a numeric default. Set DefaultValue explicitly to avoid overload ambiguity. Plain strings are values; trusted SQL expressions use RawSql.Insert.
Database.AddColumn("Orders", new Column("Amount", DbType.Decimal)
+{
+ Precision = 12, Scale = 4, IsNullable = false, DefaultValue = 0m
+});migration.Create.Column("Amount", "Orders").OfType(DbType.Decimal)
+ .WithPrecision(12, 4).NotNullable().WithDefaultValue(0m);Inside Up() / BuildUp(MigrationBuilder migration)
Use TimeOnly for time-of-day values and TimeSpan for intervals. A TimeSpan is a duration, including negative and multi-day values, so a TimeSpan default on a Time column is rejected. PostgreSQL and Oracle have native intervals; SQLite, SQL Server and MySQL/MariaDB store intervals as signed .NET ticks.
Database.AddTable("Jobs",
+ new Column("RunAt", DbType.Time) { DefaultValue = new TimeOnly(9, 30) },
+ new Column("Elapsed", MigratorDbType.Interval) { DefaultValue = TimeSpan.Zero });migration.Create.Table("Jobs")
+ .WithColumn("RunAt").OfType(DbType.Time).WithDefaultValue(new TimeOnly(9, 30))
+ .WithColumn("Elapsed").OfType(MigratorDbType.Interval).WithDefaultValue(TimeSpan.Zero);Inside Up() / BuildUp(MigrationBuilder migration)
SQLite does not enforce declared string lengths or decimal precision. UInt64 values above Int64.MaxValue are rejected there. Oracle character empty strings become NULL; Informix and Sybase have their own trimming and range behavior. Consult the type support and boundary matrix for supported mappings and live-test scope. A shared DbType does not imply identical native storage.
Choose between inspecting the live schema and declaring a provider-specific operation.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
The Classic provider indexer selects a named provider or a no-op provider. Fluent IfDatabase wraps structured operations in a provider condition. Use the provider names understood by the dialect; this SQLite example leaves other providers unchanged.
Database["SQLite"].ExecuteNonQuery("UPDATE Users SET Name = upper(Name)");migration.IfDatabase("SQLite", sqlite =>
+ sqlite.Execute.Sql("UPDATE Users SET Name = upper(Name)"));Inside Up() / BuildUp(MigrationBuilder migration)
Use Database.TableExists/ColumnExists or FluentMigration.Schema for connected checks. These inspect the current database. A fluent BuildUp method collects operations before they execute, so queued creation is not visible to a live metadata read in the same method.
For execution-time decisions after earlier operations, use an explicit provider callback. That callback cannot be previewed and requires an authored reverse. Avoid making a migration silently succeed with the wrong schema: an existence check alone does not validate a column’s type or constraint definition.
Select the connection, migration set and history scope first, then set runner options before executing.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
This host fragment assumes an open ADO.NET connection. The provider scope partitions history and selects explicitly scoped classes. An unscoped migration inherits the provider scope. Scopes do not create separate database objects: two modules can still conflict on a table name.
using var billingProvider = ProviderFactory.Create(
+ ProviderTypes.SQLite, connection, defaultSchema: null, scope: "billing");
+billingProvider.CommandTimeout = 60;
+var billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false);
+billing.SchemaInfoTableName = "BillingSchemaInfo";
+billing.Options.Tags.Add("core");
+billing.Options.TagMatch = TagMatchMode.All;
+billing.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+billing.MigrateToLastVersion();using var billingProvider = ProviderFactory.Create(
+ ProviderTypes.SQLite, connection, defaultSchema: null, scope: "billing");
+billingProvider.CommandTimeout = 60;
+var billing = new Migrator(billingProvider, typeof(CreateUsers).Assembly, false);
+billing.SchemaInfoTableName = "BillingSchemaInfo";
+billing.Options.Tags.Add("core");
+billing.Options.TagMatch = TagMatchMode.All;
+billing.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+billing.MigrateToLastVersion();Shared host · both styles
| Option | Behavior |
|---|---|
| Tags / TagMatch | Case-sensitive ordinal tags; match Any or All. No filter selects all versioned migrations. |
| Profiles | Explicit profile names; selected profiles run after versioned migrations. |
| TransactionMode | PerMigration, None or WholeSession. WholeSession supports SQLite, PostgreSQL and SQL Server. |
| Activator | Optional delegate for creating migration instances. |
| Lock / LockTimeout | Optional cross-process lease acquired before reading history; default timeout is 30 seconds. |
Set the history table before accessing history or running migrations. Its default name is SchemaInfo; the default scope is default. Give each runner its intended assembly or explicit migration types. Duplicate versions within an effective scope fail discovery.
Load connection strings from application configuration or environment variables. The CLI reads MIGRATOR_CONNECTION by default. Do not store production credentials in migration classes.
Use the active provider connection when a migration needs driver-level work.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
The provider creates a command associated with its current transaction. Dispose it after use. Generate parameter names through the provider, rather than assuming every driver uses the same convention. The callback is deferred until fluent execution reaches it.
using var command = Database.CreateCommand();
+var name = Database.GenerateParameterName(0);
+command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1";
+var value = command.CreateParameter();
+value.ParameterName = name;
+value.Value = "Ada";
+command.Parameters.Add(value);
+command.ExecuteNonQuery();migration.Execute.WithProvider(provider =>
+{
+ using var command = provider.CreateCommand();
+ var name = provider.GenerateParameterName(0);
+ command.CommandText = "UPDATE Users SET Name = " + name + " WHERE Id = 1";
+ var value = command.CreateParameter();
+ value.ParameterName = name;
+ value.Value = "Ada";
+ command.Parameters.Add(value);
+ command.ExecuteNonQuery();
+});Inside Up() / BuildUp(MigrationBuilder migration)
WithCommand creates and disposes a provider command around your action. WithConnection exposes the connection; WithProvider exposes the complete transformation provider. Do not close or replace a runner-owned connection, commit its transaction or switch databases while holding a native migration lock.
Database creation and other administration require a connection and identity authorized for that operation. Use a dedicated host with TransactionMode.None. Fluent administration rejects an active transaction; do not combine it with WholeSession or assume a Classic provider call can participate in transactional DDL.
Database.CreateDatabases("Reporting");migration.Administration.CreateDatabase("Reporting");Inside Up() / BuildUp(MigrationBuilder migration)
The remaining mappings are DropDatabases / Administration.DropDatabase, SwitchDatabase / Administration.SwitchDatabase, and KillDatabaseConnections / Administration.KillConnections. These are explicit administrative actions with provider-specific support. Database switches invalidate assumptions about migration history and session locks: keep provisioning separate from ordinary schema migrations. They are outside SQL preview and automatic reversal.
Callbacks can perform arbitrary C# work and cannot be translated into SQL preview. They require explicit reverse behavior. Keeping external network calls out of migration bodies makes failures easier to reason about: a database rollback cannot undo an email or an HTTP request.
Declare table invariants independently of column attributes.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Existing rows must satisfy a new constraint. A rebuild or ALTER operation can fail if duplicate or invalid data is present. CHECK expressions are trusted SQL and depend on the target engine. Primary keys, unique constraints, foreign keys and checks have typed definitions.
Database.AddUniqueConstraint("UQ_Users_Name", "Users", "Name");
+Database.AddCheckConstraint("CK_Users_Id", "Users", "Id > 0");migration.Create.Unique("UQ_Users_Name", "Users", "Name");
+migration.Create.Check("CK_Users_Id", "Users", "Id > 0");Inside Up() / BuildUp(MigrationBuilder migration)
Use dedicated primary-key and foreign-key removal methods; generic RemoveConstraint is for unique/check constraints in the SQLite provider. Avoid RemoveAllConstraints unless the migration deliberately replaces every invariant.
Database.RemoveConstraint("Users", "CK_Users_Id");migration.Delete.Constraint("CK_Users_Id", "Users");Inside Up() / BuildUp(MigrationBuilder migration)
GetTableConstraints returns ordered typed definitions. SQLite can return a null name for an unnamed legacy constraint; an autoindex name is not a substitute constraint name. PrimaryKeyExists checks the actual key name. MySQL reports the primary key name as PRIMARY.
Altering a column does not give that column ownership of a unique constraint. SQL Server implicit ownership markers are no longer used for deletion. Explicitly remove only the object your migration intends to change.
Make a provider change reproducible, then verify its observable behavior.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Include package, database and driver versions, a minimal migration, relevant schema/data, and expected versus actual behavior. Remove secrets from connection strings and logs. File reports in the issue tracker.
Add a regression that fails before the fix and checks the real result afterward. Provider-specific changes need actual-engine evidence; skipped tests and generated SQL alone do not qualify support. Keep mutable definition inputs independent from caller arrays and check failure paths.
Edit docs/_src/content.py for chapters and docs/_src/home.html for the homepage. Run python .github/scripts/build-docs.py to regenerate static HTML and the search index. Run python .github/scripts/verify-docs.py --compile to validate links, paired examples and compilable C# samples. Site assets live in docs/assets.
Each migration-operation example should provide Classic and Fluent versions. Shared runner and shell commands intentionally appear in both tabs. Keep provider restrictions precise and features described in the present tense. Run the homepage CI-count renderer tests when changing the site template.
The chapter organization follows the learning path of FluentMigrator’s documentation, adapted to this API. Visual references include Resend’s typography and code tabs and Gel’s code walkthroughs. The site uses its own palette, layout, copy and migration illustrations.
Describe a complete table: columns first, with explicit named keys and constraints.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
The table definition groups related schema objects into one operation. Primary-key columns are emitted as non-nullable. In the fluent API a complete table is collected before execution, so keys can refer to columns declared in the same chain.
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+ public override void Up()
+ {
+ Database.AddTable("Users",
+ new Column("Id", DbType.Int32) { IsNullable = false },
+ new Column("Name", DbType.String, 255),
+ new PrimaryKeyConstraint("PK_Users", "Id"));
+ }
+
+ public override void Down() => Database.RemoveTable("Users");
+}using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(1)]
+public class CreateUsers : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ {
+ migration.Create.Table("Users")
+ .WithColumn("Id").AsInt32().NotNullable()
+ .WithColumn("Name").AsString(255)
+ .WithPrimaryKey("PK_Users", "Id");
+ }
+
+ public override void BuildDown(MigrationBuilder migration)
+ => migration.Delete.Table("Users");
+}Choose one authoring style
Use the declared key order consistently in both primary and foreign keys. A composite unique constraint applies to the tuple; it does not make each column unique separately. The fully qualified constraint type below avoids the name collision with System.Data.UniqueConstraint.
Database.AddTable("Subscriptions",
+ new Column("TenantId", DbType.Int32),
+ new Column("UserId", DbType.Int32),
+ new Column("Email", DbType.String, 255),
+ new PrimaryKeyConstraint("PK_Subscriptions", "TenantId", "UserId"),
+ new DotNetProjects.Migrator.Framework.UniqueConstraint(
+ "UQ_Subscriptions_Email", "TenantId", "Email"));migration.Create.Table("Subscriptions")
+ .WithColumn("TenantId").AsInt32()
+ .WithColumn("UserId").AsInt32()
+ .WithColumn("Email").AsString(255)
+ .WithPrimaryKey("PK_Subscriptions", "TenantId", "UserId")
+ .WithUniqueConstraint("UQ_Subscriptions_Email", "TenantId", "Email");Inside Up() / BuildUp(MigrationBuilder migration)
Identity generation is a column attribute, separate from primary-key membership. SQLite requires an INTEGER identity column and its single-column primary key in the same definition. Use a complete Create.Table/AddTable operation to satisfy that rule. To reverse creation use Database.RemoveTable or migration.Delete.Table; dropping a table also removes its rows.
For supported creation operations, automatic reversal can derive the reverse operation. Explicitly author reverse behavior for destructive changes.
Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Column and value arrays must have the same length. The provider binds values using its driver-specific parameter mappings. For multiple rows issue multiple operations; the fluent Row method describes one row, not an accumulated collection of rows.
Database.Insert("Users", new[] { "Id", "Name" }, new object[] { 1, "Ada" });migration.Insert.IntoTable("Users")
+ .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" });Inside Up() / BuildUp(MigrationBuilder migration)
Without a predicate, update/delete affects every row. Supply predicate columns and values deliberately. Fluent WhereSql is available for updates only; its text is trusted SQL, not an escaped user input.
Database.Update("Users", new[] { "Name" }, new object[] { "Ada Lovelace" },
+ new[] { "Id" }, new object[] { 1 });migration.Update.Table("Users")
+ .Set(new[] { "Name" }, new object[] { "Ada Lovelace" })
+ .Where(new[] { "Id" }, new object[] { 1 });Inside Up() / BuildUp(MigrationBuilder migration)
Database.Delete("Users", new[] { "Id" }, new object[] { 1 });migration.Delete.FromTable("Users").Where(new[] { "Id" }, new object[] { 1 });Inside Up() / BuildUp(MigrationBuilder migration)
Use an explicit identifying predicate when a seed should exist only once. This is distinct from a migration version: a named profile can run repeatedly without a history entry. Coordinate competing writers; a check-then-insert helper is not a substitute for a database unique key.
Database.InsertIfNotExists("Users", new[] { "Id", "Name" },
+ new object[] { 1, "Ada" }, new[] { "Id" }, new object[] { 1 });migration.Insert.IntoTable("Users")
+ .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" })
+ .IfNotExists(new[] { "Id" }, new object[] { 1 });Inside Up() / BuildUp(MigrationBuilder migration)
Use the provider CopyDataFromTableToTable helper or fluent Execute.CopyData for named-column copies. Both tables must already exist and target columns must accept the source values. Execute.UpdateFrom maps source/target pairs. These operations retain provider limits and are outside the SQL-preview subset. A reverse data migration needs authored recovery logic; auto-reversal cannot recreate deleted or overwritten values.
Database.CopyDataFromTableToTable("Users",
+ new System.Collections.Generic.List<string> { "Id", "Name" }, "ArchivedUsers",
+ new System.Collections.Generic.List<string> { "UserId", "DisplayName" });migration.Execute.CopyData("Users", new[] { "Id", "Name" },
+ "ArchivedUsers", new[] { "UserId", "DisplayName" });Inside Up() / BuildUp(MigrationBuilder migration)
Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Ordinary strings are quoted literal values. RawSql.Insert marks trusted SQL to evaluate on the database. The expression below works on SQLite; provider function names and return types can differ.
Database.AddTable("Events", new Column("CreatedAt", DbType.DateTime)
+{
+ DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP"), IsNullable = false
+});migration.Create.Table("Events")
+ .WithColumn("CreatedAt").OfType(DbType.DateTime).NotNullable()
+ .WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP"));Inside Up() / BuildUp(MigrationBuilder migration)
Collation presets request semantics. Unsupported mappings fail before DDL. SQLite AsciiIgnoreCase maps to NOCASE and folds ASCII only; it is not Unicode case folding. Named custom SQLite collations must be registered on the connection before schema or data operations use them.
Database.AddTable("Labels", new Column("Name", DbType.String, 100)
+{
+ Collation = Collation.AsciiIgnoreCase
+});migration.Create.Table("Labels")
+ .WithColumn("Name").AsString(100)
+ .WithCollation(Collation.AsciiIgnoreCase);Inside Up() / BuildUp(MigrationBuilder migration)
| Request | Meaning |
|---|---|
| CaseInsensitive / CaseSensitive | Case behavior with accent sensitivity; supported SQL Server/MySQL/MariaDB mappings, or an explicit installed name on other engines. |
| Binary | Provider binary comparison; not a promise of identical linguistic ordering. |
| AsciiIgnoreCase | SQLite NOCASE; ASCII letters only. |
| Collation.Named(name) | An installed or registered provider-specific collation. |
Use named collations for language-specific or exact comparison behavior. PostgreSQL ICU nondeterministic collations must be created explicitly; SQL rendering does not create shared database objects. Read the mapping table for engine versions and restrictions.
Resolve the runner and migration dependencies inside one service scope.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Install DotNetProjects.Migrator.Extensions.DependencyInjection and Microsoft.Extensions.Logging alongside the core and database driver. This example uses the connection-string provider factory so provider disposal belongs to the DI scope. The providerName explicitly selects the SQLite driver.
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using DotNetProjects.Migrator.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+services.AddLogging();
+services.AddMigrator(_ => ProviderFactory.Create(
+ ProviderTypes.SQLite, "Data Source=app.db", defaultSchema: null,
+ providerName: "Microsoft.Data.Sqlite"), typeof(CreateUsers).Assembly,
+ options => options.TransactionMode = MigrationTransactionMode.PerMigration);
+
+using var container = services.BuildServiceProvider();
+using var scope = container.CreateScope();
+scope.ServiceProvider.GetRequiredService<Migrator>().MigrateToLastVersion();using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using DotNetProjects.Migrator.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+services.AddLogging();
+services.AddMigrator(_ => ProviderFactory.Create(
+ ProviderTypes.SQLite, "Data Source=app.db", defaultSchema: null,
+ providerName: "Microsoft.Data.Sqlite"), typeof(CreateUsers).Assembly,
+ options => options.TransactionMode = MigrationTransactionMode.PerMigration);
+
+using var container = services.BuildServiceProvider();
+using var scope = container.CreateScope();
+scope.ServiceProvider.GetRequiredService<Migrator>().MigrateToLastVersion();Shared host · both styles
Migration classes are registered for activation through the service provider. Register your own constructor dependencies before resolving the runner. Options are scoped snapshots; a custom Activator can override construction. Fluent and Classic migrations use the same activation mechanism.
The integration adapts runner lifecycle events to Microsoft logging. It omits SQL text and raw exception messages from these events. Configure your own logging providers through AddLogging. The core retains its lightweight logger API when you do not use DI.
Dispose the scope after migration execution. When supplying a caller-owned open connection, keep its owner alive until after the scope is disposed; the provider does not acquire ownership of an externally supplied connection.
Reuse schema conventions without hiding provider behavior or changing the migration contract.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
RunnerOptions.Activator constructs migrations when a DI container is not appropriate. IMigrationLock supplies a disposable lease for custom deployment coordination. Release must work on success and failure. Configure these at the host boundary rather than in individual migrations.
Decisions to make before adopting the library or moving an existing migration project.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
No. Migrations operate on an ADO.NET connection through the transformation provider. Use EF, Dapper, another data layer or direct SQL in the rest of your application. Migrator does not scaffold schema changes from an object model.
Yes. Both implement the same migration contract, run through the same loader and share history. Keep each version unique. The tabs throughout these guides show equivalent choices, not two classes to install together. The authoring method names differ: Up/Down versus BuildUp/BuildDown.
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+ public override void Up()
+ {
+ Database.AddTable("Users",
+ new Column("Id", DbType.Int32) { IsNullable = false },
+ new Column("Name", DbType.String, 255),
+ new PrimaryKeyConstraint("PK_Users", "Id"));
+ }
+
+ public override void Down() => Database.RemoveTable("Users");
+}using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(1)]
+public class CreateUsers : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ {
+ migration.Create.Table("Users")
+ .WithColumn("Id").AsInt32().NotNullable()
+ .WithColumn("Name").AsString(255)
+ .WithPrimaryKey("PK_Users", "Id");
+ }
+
+ public override void BuildDown(MigrationBuilder migration)
+ => migration.Delete.Table("Users");
+}Choose one authoring style
SQLite does not implement every ALTER TABLE operation. Migrator reads the live schema and reconstructs a supported table when a column or constraint change needs it. This is useful without an ORM model. See SQLite for preserved objects, foreign-key checks and reconstruction boundaries.
A transaction can roll back a failed migration when its database operations are transactional. Downgrading a completed version executes your reverse method. Neither mechanism recovers rows already deleted by a successful migration. Use an explicit recovery design and backups for that case.
The journal records versions, scopes and timestamps, not a content checksum. Editing an applied class will not make it rerun. Add a new migration for a change. Consolidated baselines are an explicit history operation; read versioning and history.
Preview renders a structured subset. A provider callback, unsupported constraint alteration or schema dependency after raw SQL cannot be represented reliably and raises an error. Read planning and SQL preview rather than treating preview as a full execution simulation.
Define ordered child/parent columns and independent actions for update and delete.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Parent key columns must identify a suitable primary/unique key. Child and parent arrays are positional: each child column corresponds to the parent column at the same index. Both tables and their compatible columns must already exist for this example. The Classic example uses IForeignKeyActions for independent update/delete actions; the older AddForeignKey overload supplies one action for both.
((IForeignKeyActions)Database).AddForeignKey(
+ "FK_Orders_Users", "Orders", new[] { "UserId" },
+ "Users", new[] { "Id" }, ForeignKeyConstraintType.Cascade,
+ ForeignKeyConstraintType.NoAction);migration.Create.ForeignKey("FK_Orders_Users",
+ "Orders", new[] { "UserId" }, "Users", new[] { "Id" },
+ onDelete: ForeignKeyConstraintType.Cascade,
+ onUpdate: ForeignKeyConstraintType.NoAction);Inside Up() / BuildUp(MigrationBuilder migration)
Remove dependent keys before incompatible table or key changes. Restore them only after the existing data satisfies the replacement relationship.
Database.RemoveForeignKey("Orders", "FK_Orders_Users");migration.Delete.ForeignKey("FK_Orders_Users", "Orders");Inside Up() / BuildUp(MigrationBuilder migration)
Supported actions depend on the database; do not assume every engine implements CASCADE, RESTRICT, SET NULL and SET DEFAULT identically. SQLite rebuilds preserve separate update/delete actions and validate integrity before an owned transaction commits. MATCH FULL and MATCH PARTIAL requests are rejected because SQLite does not enforce those semantics.
SetNull needs nullable child columns. Test action behavior using actual data, especially composite keys and partially NULL values. Oracle supports its own subset of foreign-key actions.
DOTNETPROJECTS / THE MIGRATION MANUAL
From your first table to deployment locks and SQLite reconstruction. Practical guides with a Classic and Fluent example for every authoring task.
Start with a working example ↗Create a SQLite database, apply a versioned change, and write its reverse. Choose either C# style; the runner is the same.
The core library, database driver, optional DI integration and CLI each have a distinct job.
Select the connection, migration set and history scope first, then set runner options before executing.
Decisions to make before adopting the library or moving an existing migration project.
Describe a complete table: columns first, with explicit named keys and constraints.
Rename objects and evolve populated tables while preserving the schema details you still need.
Type, size, precision, nullability, defaults, identity and collation are explicit column attributes.
Insert, update and delete using explicit column/value arrays. Keep predicates separate from changed values.
Read the connected database before deciding what to change. Metadata is different from a model snapshot.
Use schema operations where they fit, and keep database-specific SQL explicit.
Use the active provider connection when a migration needs driver-level work.
An index is a separate schema object, even when it enforces uniqueness.
Declare table invariants independently of column attributes.
Define ordered child/parent columns and independent actions for update and delete.
Distinguish values from SQL expressions, and comparison intent from a provider's installed collation name.
Use the same migration assembly in a dedicated host, a DI scope or the command-line tool.
List, validate, plan, preview, apply and reverse migrations from a deployment script.
Resolve the runner and migration dependencies inside one service scope.
A version plan answers what runs. SQL preview shows the supported operation SQL.
Transaction rollback and cross-process coordination solve different problems.
Number changes, keep applied source immutable and give independent modules explicit histories.
Select a subset of versioned migrations using explicit ordinal names.
Run explicitly selected work after versioned migrations without recording a version.
Place ordered work at the runner's lifecycle stages.
Fluent operations can describe a supported reverse sequence; Classic migrations author it directly.
One authoring contract, explicit database behavior. Choose the driver and provider together.
Change existing tables from the live schema, without maintaining an ORM model.
Explicit keys, provider-specific indexes, transactional DDL and application locks.
Native intervals, schema-aware metadata, transactional DDL and advisory locks.
Related providers with explicit engine, collation and DDL transaction differences.
Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.
Use the live-engine matrix to qualify operations beyond the common database families.
Choose between inspecting the live schema and declaring a provider-specific operation.
Reuse schema conventions without hiding provider behavior or changing the migration contract.
Verify stored data, preserved schema and repeat execution on the actual target engine.
Update source definitions while preserving the history your databases already contain.
A practical index of the two authoring surfaces and their shared provider contracts.
Make a provider change reproducible, then verify its observable behavior.
An index is a separate schema object, even when it enforces uniqueness.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Use an explicit name so the index can be inspected or removed later. Both APIs accept the same Index definition. Fully qualify this type if System.Index is also in scope. Columns retain the order in KeyColumns.
Database.AddIndex("Users", new DotNetProjects.Migrator.Framework.Index
+{
+ Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false
+});migration.Create.Index("Users", new DotNetProjects.Migrator.Framework.Index
+{
+ Name = "IX_Users_Name", KeyColumns = new[] { "Name" }, Unique = false
+});Inside Up() / BuildUp(MigrationBuilder migration)
Database.RemoveIndex("Users", "IX_Users_Name");migration.Delete.Index("IX_Users_Name", "Users");Inside Up() / BuildUp(MigrationBuilder migration)
Index definitions also expose IncludeColumns, FilterItems and Clustered. These options are provider-specific. Oracle rejects included and clustered index requests; SQLite reconstruction rejects existing index SQL with explicit COLLATE clauses. Preview handles simple indexes and rejects unsupported options.
Use UniqueConstraint for a table-level invariant and an Index with Unique for an index definition. Do not infer ownership from a generated name. SQLite RemoveAllIndexes preserves declared table UNIQUE constraints; remove those through the constraint APIs. Check query plans and data cardinality when choosing index keys.
The core library, database driver, optional DI integration and CLI each have a distinct job.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
| Package | Purpose |
|---|---|
| DotNetProjects.Migrator | Migration classes, providers, runner and fluent operations. |
| An ADO.NET driver | Install the driver for the database your host opens. |
| DotNetProjects.Migrator.Extensions.DependencyInjection | Optional scoped runner, constructor injection and Microsoft logging. |
| DotNetProjects.Migrator.Tool | The migrator command-line tool. |
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7Shared commands · both styles
Common choices are Microsoft.Data.Sqlite, Microsoft.Data.SqlClient, Npgsql, MySql.Data, Oracle.ManagedDataAccess.Core and FirebirdSql.Data.FirebirdClient. The core library does not directly reference these packages. Passing an open connection makes driver selection explicit and keeps connection ownership with your application.
Read the provider overview for database families, aliases and CI coverage. A provider name is not a guarantee that every native operation has the same behavior on every server.
To develop against a checkout, replace the core package reference with a project reference to src/Migrator/DotNetProjects.Migrator.csproj. The solution targets .NET 9. Building the .slnx solution requires an SDK that understands that format, such as SDK 9.0.200 or later.
Keep the library, CLI and optional DI integration on compatible versions. Recompile old migration assemblies when updating a breaking API; the upgrade guide explains the column and constraint changes.
Place ordered work at the runner's lifecycle stages.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
| Stage | When |
|---|---|
| BeforeRun | Before versioned migration work in this run. |
| BeforeMigration | Before each executed versioned migration. |
| AfterMigration | After each executed versioned migration. |
| AfterRun | After the run's migration/profile work. |
Maintenance classes accept Order and Scope. They use Up and do not acquire version records. Hooks stop on failure; later stages are not finally blocks or guaranteed cleanup paths. Lock release and connection/transaction restoration are runner responsibilities.
The example expects an existing DeploymentLog table. Choose a table that already exists at the selected stage. Fluent callbacks execute at the corresponding operation position.
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Maintenance(MaintenanceStage.AfterRun, Order = 10)]
+public class RecordDeployment : Migration
+{
+ public override void Up()
+ => Database.Insert("DeploymentLog", new[] { "Message" }, new object[] { "Migration run finished" });
+ public override void Down() { }
+}using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Maintenance(MaintenanceStage.AfterRun, Order = 10)]
+public class RecordDeployment : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ => migration.Insert.IntoTable("DeploymentLog")
+ .Row(new[] { "Message" }, new object[] { "Migration run finished" });
+ public override void BuildDown(MigrationBuilder migration) { }
+}Choose one authoring style
Migration.AfterUp and AfterDown run after commit, with the migration context restored. In WholeSession they wait until the entire session commits. Their failure reports an error after durable changes; it cannot reverse that commit. Do not confuse maintenance stages with a guaranteed post-commit delivery system.
Related providers with explicit engine, collation and DDL transaction differences.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Select ProviderTypes.Mysql for MySQL and MariaDB for MariaDB. Use an open driver connection or configure the factory. Do not treat compatible wire protocols as proof of identical server syntax or metadata behavior. DDL can commit implicitly; the runner rejects WholeSession for these dialects.
Semantic presets require utf8mb4-compatible text and the documented server versions: MySQL 8 and MariaDB 10.10+ have different mappings. Use a named installed collation if exact linguistic or trailing-space behavior matters.
Database.AddTable("Labels", new Column("Name", DbType.String, 100)
+{
+ Collation = Collation.CaseInsensitive
+});migration.Create.Table("Labels").WithColumn("Name").AsString(100)
+ .WithCollation(Collation.CaseInsensitive);Inside Up() / BuildUp(MigrationBuilder migration)
MySQL reports primary keys as PRIMARY even if the migration supplied a symbolic name. MySQL/MariaDB catalogs expose unique indexes as unique constraints, so metadata cannot recover every original CREATE UNIQUE INDEX versus UNIQUE-clause choice. Do not derive ownership from that distinction.
DatabaseMigrationLock uses named session locks. These coordinate one server, not a distributed cluster. Interval values use signed .NET ticks. String overflow behavior depends on SQL mode; boundary CI uses STRICT_ALL_TABLES. Check server settings when evaluating length and decimal errors.
Preserve explicit constraints and be deliberate about identity, sequences and implicit DDL commits.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Use ProviderTypes.Oracle with the Oracle managed ADO.NET driver and the intended schema. MsOracle is a historical variant. Oracle DDL is not generally atomic across a migration; WholeSession is rejected. Some quoted qualified metadata lookups are explicitly rejected.
Identity is a column attribute and is validated before table creation. It need not be a primary key on every engine, but the example pairs it with an explicit key. Use a server/driver combination qualified for native identity.
Database.AddTable("Entries",
+ new Column("Id", DbType.Int32) { IsIdentity = true, IsNullable = false },
+ new Column("Text", DbType.String, 255),
+ new PrimaryKeyConstraint("PK_Entries", "Id"));migration.Create.Table("Entries")
+ .WithColumn("Id").AsInt32().Identity().NotNullable()
+ .WithColumn("Text").AsString(255)
+ .WithPrimaryKey("PK_Entries", "Id");Inside Up() / BuildUp(MigrationBuilder migration)
RemoveTable leaves unrelated sequences intact. Oracle removes table-owned triggers and native identity objects. For a legacy sequence that the migration explicitly owns, OracleTransformationProvider.RemoveTableWithOwnedSequences validates named sequences and propagates cleanup errors. It does not infer sequence ownership from naming patterns.
Oracle empty character strings become NULL. Time uses DATE with a fixed 1970-01-01 date and whole-second precision; fractional Time inputs are rejected. Intervals use native storage. Changes that require an unsupported in-place type conversion need an explicit data migration.
Included/clustered index options are rejected rather than ignored. Ordered foreign-key pairs and delete actions are preserved by structured metadata. A SQL Server clustered-index request is not translated into an Oracle index-organized table.
Use the live-engine matrix to qualify operations beyond the common database families.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
ProviderTypes.Hana uses SAP’s native .NET driver. The CI job runs HANA Express and exercises schema/data operations, metadata, constraints, history, restart and DML rollback. DDL may autocommit. Use a custom host; the CLI driver bundle does not include an online HANA host.
HANA has its own supported type set; Guid and DateTimeOffset are outside the current matrix mappings. Review the type matrix before choosing shared column definitions.
| Provider | Things to check |
|---|---|
| Db2 LUW | Driver runtime dependencies, decimal/storage capacity and ordinary/unique index options. |
| Informix | Native driver and database encoding; TEXT reads, integer NULL sentinels, whole-second Time and trailing-space trimming. |
| Firebird | Decimal storage capacity, ordinary/unique index operations and transaction behavior. |
| Sybase ASE | TEXTSIZE and string truncation settings, nullable BIT restrictions, trimmed strings and constraint-name limitations. |
Ingres remains a source dialect outside the eleven-engine matrix. Redshift, Snowflake and Db2 for IBM i require separate provider/infrastructure qualification; PostgreSQL tests do not qualify Redshift, and Db2 LUW tests do not qualify IBM i.
See qualification requirements and live test setup for exact coverage and reproduction commands.
Native intervals, schema-aware metadata, transactional DDL and advisory locks.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Use ProviderTypes.PostgreSQL and an open Npgsql connection, with the intended default schema. Connection search_path affects unqualified relation lookup. Metadata readers resolve the requested relation through PostgreSQL and distinguish same-named tables in different schemas.
PostgreSQL maps duration values to native intervals. Time without time zone maps to a time of day; use TimeOnly for that input. Parameter mappings and scalar CLR return types are separate concerns: raw ADO.NET values remain driver-specific.
Database.AddColumn("Jobs", new Column("Elapsed", MigratorDbType.Interval)
+{
+ DefaultValue = TimeSpan.FromDays(2)
+});migration.Create.Column("Elapsed", "Jobs").OfType(MigratorDbType.Interval)
+ .WithDefaultValue(TimeSpan.FromDays(2));Inside Up() / BuildUp(MigrationBuilder migration)
Create any ICU nondeterministic collation explicitly, then select it with Collation.Named. Column rendering does not silently create shared collation objects. Binary maps to C; language and case semantics should use a specific installed name.
Schema-aware metadata does not establish complete qualification for every operation. Test quoted names and search-path behavior with your migration. Renaming a table retains its named constraints; avoid colliding names when recreating the old table.
WholeSession is supported for verified transactional DDL, and DatabaseMigrationLock uses a session advisory lock. Statements that require special transaction treatment need a separate deployment design. Keep the connection stable while the lease is held.
A version plan answers what runs. SQL preview shows the supported operation SQL.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Plan and DryRun inspect applied versions through IMigrationHistory without creating/upgrading history or invoking migration bodies, callbacks, transactions or SQLite PRAGMA changes. Set the same scope, tags and assembly you intend to deploy. The fragment below assumes an initialized runner.
var plan = runner.Plan(10);
+foreach (var step in plan)
+ Console.WriteLine($"{step.Version}: {(step.IsUp ? "up" : "down")}");
+runner.DryRun = true;
+runner.MigrateTo(10);var plan = runner.Plan(10);
+foreach (var step in plan)
+ Console.WriteLine($"{step.Version}: {(step.IsUp ? "up" : "down")}");
+runner.DryRun = true;
+runner.MigrateTo(10);Shared host · both styles
PreviewSql reads connected history and schema. Classic bodies require explicit opt-in; provider calls are captured through a proxy that rejects unsupported access. Fluent authoring builds operations directly. This is trusted C# execution in both cases, not a security sandbox.
var sql = runner.PreviewSql(10, ProviderTypes.SQLite, allowLegacyBodies: true);
+Console.WriteLine(sql);var sql = runner.PreviewSql(10, ProviderTypes.SQLite);
+Console.WriteLine(sql);Choose one authoring style
MigrationSqlPreview.Generate can render supported operations without connecting; the CLI exposes --offline. Earlier structured create/rename operations update the planned schema. Raw SQL invalidates that knowledge, so later dependencies can fail.
Basic tables/columns, supported renames, simple indexes, inserts and raw SQL form the preview subset. Unsupported alterations, constraint changes, filters, callbacks and schema dependencies throw. InitializeOnce overrides are rejected rather than skipped silently. Post-commit callbacks do not run. Output contains operation SQL, not history guards or an idempotent deployment bundle.
Run explicitly selected work after versioned migrations without recording a version.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
A profile is useful for optional seed data or environment setup. It runs every time its name is selected. Make repeated execution deliberate: use an identifying predicate or other idempotent operation where appropriate.
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Profile("demo", Order = 10)]
+public class DemoData : Migration
+{
+ public override void Up() => Database.InsertIfNotExists("Users",
+ new[] { "Id", "Name" }, new object[] { 1, "Ada" },
+ new[] { "Id" }, new object[] { 1 });
+ public override void Down() { }
+}using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Profile("demo", Order = 10)]
+public class DemoData : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ => migration.Insert.IntoTable("Users")
+ .Row(new[] { "Id", "Name" }, new object[] { 1, "Ada" })
+ .IfNotExists(new[] { "Id" }, new object[] { 1 });
+ public override void BuildDown(MigrationBuilder migration) { }
+}Choose one authoring style
runner.Options.Profiles.Add("demo");
+runner.MigrateToLastVersion();runner.Options.Profiles.Add("demo");
+runner.MigrateToLastVersion();Shared host · both styles
Profiles accept Order and Scope. Execution orders by Order and then ordinal full type name. Profile execution uses Up and does not create a migration-version entry or use Down as an undo history. An auxiliary-only run preserves existing version history.
A selected profile runs because it was selected, not because its source checksum changed. Treat this separately from versioned migrations and checksum-based repeatable SQL. Offline CLI SQL generation rejects profiles because it cannot represent the complete lifecycle.
One authoring contract, explicit database behavior. Choose the driver and provider together.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
| Database | ProviderTypes | Guide |
|---|---|---|
| SQLite | SQLite / MonoSQLite | Live-schema reconstruction |
| SQL Server | SqlServer / SqlServer2005 | Constraints, batches and locks |
| PostgreSQL | PostgreSQL / PostgreSQL82 | Schemas, types and locks |
| MySQL / MariaDB | Mysql / MariaDB | DDL and collation behavior |
| Oracle | Oracle / MsOracle | Identity and metadata |
| SAP HANA | Hana | Additional providers |
| Db2 / Informix / Firebird / Ingres / Sybase | IBM_DB2 / IBM_Informix / Firebird / Ingres / Sybase | Engine-specific guidance |
Pass an open IDbConnection to ProviderFactory.Create. Both migration styles use that provider. Alternatively use the connection-string overload and configure providerName so the provider can resolve the ADO.NET factory. Use a matching driver and test the exact server version you deploy.
using var selectedProvider = ProviderFactory.Create(
+ ProviderTypes.PostgreSQL, connection, defaultSchema: "public", scope: "billing");
+var selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false);
+selectedRunner.MigrateToLastVersion();using var selectedProvider = ProviderFactory.Create(
+ ProviderTypes.PostgreSQL, connection, defaultSchema: "public", scope: "billing");
+var selectedRunner = new Migrator(selectedProvider, typeof(CreateUsers).Assembly, false);
+selectedRunner.MigrateToLastVersion();Shared host · both styles
The CI matrix includes SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase and SAP HANA. Ingres and historical provider aliases have separate qualification needs. Read testing and the live-engine matrix for exact drivers and server setup.
Database support is operation-specific. Column types, collation presets, index options, DDL transactions and metadata readers can differ. Test stored values and preserved schema, not only the generated SQL.
Create a SQLite database, apply a versioned change, and write its reverse. Choose either C# style; the runner is the same.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Start with the .NET 9 SDK and a console application. The core package supplies schema operations; your ADO.NET package connects to the database. SQLite needs no separate database server for this example.
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7Shared commands · both styles
Add CreateUsers.cs. Choose one tab and copy that class. Each public migration has a numeric version; do not put both versions of the same example into one assembly. Classic migrations execute provider methods in Up and Down. Fluent migrations collect structured operations in BuildUp and BuildDown.
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+ public override void Up()
+ {
+ Database.AddTable("Users",
+ new Column("Id", DbType.Int32) { IsNullable = false },
+ new Column("Name", DbType.String, 255),
+ new PrimaryKeyConstraint("PK_Users", "Id"));
+ }
+
+ public override void Down() => Database.RemoveTable("Users");
+}using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(1)]
+public class CreateUsers : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ {
+ migration.Create.Table("Users")
+ .WithColumn("Id").AsInt32().NotNullable()
+ .WithColumn("Name").AsString(255)
+ .WithPrimaryKey("PK_Users", "Id");
+ }
+
+ public override void BuildDown(MigrationBuilder migration)
+ => migration.Delete.Table("Users");
+}Choose one authoring style
Replace Program.cs with the shared host below and run dotnet run. The open connection belongs to this host and is disposed after the provider. The runner discovers public migration classes in the selected assembly.
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+ ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+ ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();Shared host · both styles
The result is an app.db file containing Users and SchemaInfo. Run again: version 1 is already recorded, so it is skipped. Add a class with [Migration(2)] for your next change.
Call runner.MigrateTo(0) to execute the reverse methods for this migration set. Here that drops Users and its data. A reverse migration is a schema operation, not a restore of deleted rows. Test both directions on a disposable database before deployment.
Continue with creating tables, or configure scopes, filters and transaction behavior.
Use the same migration assembly in a dedicated host, a DI scope or the command-line tool.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
| Runner | A good fit |
|---|---|
| Library host | A small deployment executable with explicit connection ownership and full provider access. |
| Microsoft DI integration | A service collection supplying constructor dependencies, options and logging. |
| migrator CLI | Automation that selects assemblies, providers, scopes, tags and target versions. |
Run schema changes before application instances need the new schema. The host below works with either migration style and scans the assembly containing CreateUsers. Use explicit type selection when an assembly also contains migrations for other purposes.
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+ ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+using var provider = ProviderFactory.Create(
+ ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var runner = new Migrator(provider, typeof(CreateUsers).Assembly, trace: false);
+runner.MigrateToLastVersion();Shared host · both styles
Give the deployment identity the schema privileges needed by the selected migrations. Coordinate concurrent deploys through an external orchestrator or supported native lock. Configure the history table and scope consistently across invocations. Log the target and result without exposing connection strings.
Choose the CLI for a ready command surface, or DI for application services. Read transaction and lock semantics before relying on atomicity.
Read the connected database before deciding what to change. Metadata is different from a model snapshot.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Classic migrations read through Database. FluentMigration exposes Schema for queries and Context for the full provider API. A fluent authoring method runs before its queued operations: an inspection cannot see a table merely queued earlier in the same builder.
if (!Database.ColumnExists("Users", "Email"))
+ Database.AddColumn("Users", new Column("Email", DbType.String, 320));if (!Schema.Table("Users").ColumnExists("Email"))
+ migration.Create.Column("Email", "Users").AsString(320);Inside Up() / BuildUp(MigrationBuilder migration)
GetColumns returns inferred column attributes, not primary/unique membership flags. It is obsolete because native types and defaults cannot be mapped back to exact .NET definitions; use migration history for the original definition. Read typed table constraints to retain ordered composite keys. Unique indexes remain index metadata. MySQL/MariaDB catalogs cannot distinguish every original unique-index versus UNIQUE-clause authoring choice.
var constraints = Database.GetTableConstraints("Users");
+foreach (var constraint in constraints)
+ Console.WriteLine(constraint.Name);var constraints = Schema.Table("Users").ConstraintDefinitions();
+foreach (var constraint in constraints)
+ Console.WriteLine(constraint.Name);Inside Up() / BuildUp(MigrationBuilder migration)
ViewField selects columns from a base table. The alternative IViewElement overload represents explicit columns and joins. View definitions are provider-dependent and outside SQL preview and automatic reversal. Write a provider-appropriate DROP VIEW statement in the reverse method, and manage dependent views when changing their underlying tables.
Database.AddView("UserNames", "Users", new ViewField("Id"), new ViewField("Name"));migration.Create.View("UserNames", "Users", new ViewField("Id"), new ViewField("Name"));Inside Up() / BuildUp(MigrationBuilder migration)
Dispose readers and commands obtained from the provider. Fluent Schema.Query and Select accept a reader callback and handle disposal. Use provider quoting helpers for table and column identifiers separately: quoting a table may introduce schema qualification, which is not valid for a column expression.
Metadata fidelity depends on the provider. Unsupported readers throw instead of pretending that an empty schema was found. A successful existence check is not a full schema-drift report.
Explicit keys, provider-specific indexes, transactional DDL and application locks.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Use ProviderTypes.SqlServer with an open Microsoft.Data.SqlClient connection. Pass the intended default schema, commonly dbo. Historical SqlServer2005 is a separate alias with older type mappings. WholeSession transactions and DatabaseMigrationLock are available for SQL Server.
Column changes preserve explicit constraints and indexes. Add/remove uniqueness independently. For a nonclustered primary key on an existing compatible table use the dedicated API shown below. Review existing clustered indexes before changing key layout.
Database.AddPrimaryKeyNonClustered("PK_Users", "Users", "Id");migration.Create.NonClusteredPrimaryKey("PK_Users", "Users", "Id");Inside Up() / BuildUp(MigrationBuilder migration)
Index definitions can express included/filter/cluster options where supported. The script APIs split standalone GO lines; raw ExecuteNonQuery/Execute.Sql does not. SQLCMD directives and GO repetition are rejected before executing script batches. Prefer scripts for client batch syntax and commands for parameterized statements.
Use TimeOnly for time values and TimeSpan for interval ticks. SqlServer2005 uses its older DATETIME precision behavior. Use separate quoting helpers for table and column names. A table rename leaves named constraints/indexes attached with their old names; assign distinct names when creating a replacement table.
Use schema operations where they fit, and keep database-specific SQL explicit.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Raw SQL passes through to the selected database. It does not translate between dialects. Values from application input should be bound through a command; migration SQL is trusted application code.
Database.ExecuteNonQuery("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL");migration.Execute.Sql("UPDATE Users SET Name = 'Unknown' WHERE Name IS NULL");Inside Up() / BuildUp(MigrationBuilder migration)
ExecuteScript reads a file; ExecuteResourceScript reads an assembly resource. Fluent equivalents capture script text as dedicated operations. Make files available at deployment and set resource names explicitly. Relative file paths are resolved against the process working directory.
Database.ExecuteScript("Scripts/backfill.sql");migration.Execute.Script("Scripts/backfill.sql");Inside Up() / BuildUp(MigrationBuilder migration)
Database.ExecuteResourceScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql");migration.Execute.EmbeddedScript(GetType().Assembly, "MyMigrations.Scripts.backfill.sql");Inside Up() / BuildUp(MigrationBuilder migration)
For the second example mark backfill.sql as an EmbeddedResource in the migration project and use its actual manifest resource name. Missing resources fail before script execution.
The script APIs split standalone SQL Server GO lines, including optional line comments, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail before any batches execute. ExecuteNonQuery and Execute.Sql do not split client separators.
Other providers receive one command unless they implement IScriptBatchProvider. A database SQL file is not necessarily compatible with SQL*Plus, mysql-client or isql command syntax. Raw SQL also invalidates planned schema knowledge during SQL preview.
Change existing tables from the live schema, without maintaining an ORM model.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
For supported changes, Migrator reads SQLiteTableInfo, changes its representation, creates a replacement table, copies mapped rows, swaps tables and recreates represented dependent objects. This provides column type/default/nullability changes and adding/removing primary, foreign, unique and check constraints.
Native rename and eligible drop-column paths are used when supported by the engine. Complex alterations use reconstruction. Existing rows must satisfy the new definition; a default does not rewrite every existing NULL during a column change.
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
+{
+ IsNullable = false, DefaultValue = "Unknown",
+ Collation = Collation.AsciiIgnoreCase
+});migration.Alter.Column("Name", "Users")
+ .AsString(500).NotNullable().WithDefaultValue("Unknown")
+ .WithCollation(Collation.AsciiIgnoreCase);Inside Up() / BuildUp(MigrationBuilder migration)
| Detail | Behavior |
|---|---|
| Mapped data | Named-column copy preserves mapped values, subject to the new definition accepting them. |
| Keys and constraints | Named/composite keys, ordered foreign-key pairs and separate update/delete actions are retained. |
| Column collations | Declared names are retained. Register custom collations on the connection. |
| Indexes and triggers | Supported definitions are recreated; unsafe trigger rename/drop-column cases are rejected. |
| AUTOINCREMENT | The sequence high-water mark survives, including previously deleted identities. |
| Hidden rowid | Not part of the mapped data and may change. |
Reconstruction rejects generated columns, STRICT, WITHOUT ROWID and indexes with explicit COLLATE clauses. It is not an arbitrary SQL dependency rewriter. Adjust dependent views, complex expressions and triggers explicitly when required. MATCH FULL and MATCH PARTIAL are rejected because SQLite does not enforce their semantics.
Owned rebuild transactions and runner transactions validate foreign-key integrity before commit and restore the prior enforcement setting. For caller-owned active transactions configure foreign keys before beginning the transaction. A SQLite write lock is not a session-wide migration lease; coordinate deployment externally or provide IMigrationLock.
CLR Guid defaults use blobs from Guid.ToByteArray(), matching inserted parameters. Legacy text GUID defaults remain SQL expressions during unrelated rebuilds, so storage is not silently converted. Convert mixed text/blob keys explicitly and consistently across related tables.
SQLite INTEGER is signed 64-bit. Declared text lengths and decimal precision do not impose SQL Server-like enforcement. An identity needs a single INTEGER primary key in the same definition. For adding identity to an existing table, use an atomic SQLite RecreateTable definition containing both objects.
FluentMigrator leaves general column alterations and later foreign-key changes to manual reconstruction. DbUp and Evolve run supplied scripts. EF Core also rebuilds SQLite tables using model-represented artifacts. Migrator reconstructs from live metadata without an ORM. The sourced operation comparison distinguishes native SQL, emulation and manual work.
Select a subset of versioned migrations using explicit ordinal names.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Tags is in DotNetProjects.Migrator. One class can declare multiple names. Choose names for deployment intent such as core or reporting; do not use a tag to hide a dependency that a selected migration still requires.
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(2), Tags("reporting")]
+public class CreateReportLog : Migration
+{
+ public override void Up() => Database.AddTable("ReportLog", new Column("Name", DbType.String, 255));
+ public override void Down() => Database.RemoveTable("ReportLog");
+}using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(2), Tags("reporting")]
+public class CreateReportLog : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ => migration.Create.Table("ReportLog").WithColumn("Name").AsString(255);
+ public override void BuildDown(MigrationBuilder migration)
+ => migration.Delete.Table("ReportLog");
+}Choose one authoring style
runner.Options.Tags.Add("reporting");
+runner.Options.TagMatch = TagMatchMode.Any;
+runner.MigrateToLastVersion();runner.Options.Tags.Add("reporting");
+runner.Options.TagMatch = TagMatchMode.Any;
+runner.MigrateToLastVersion();Shared host · both styles
Any requires at least one selected tag; All requires every selected tag. Matching is ordinal and case-sensitive. Without a tag filter all eligible versioned migrations are selected. Profiles have their own explicit name selection.
Applied versions excluded by the active filter remain applied during downgrade. A filtered run is therefore not a promise that the whole database matches one contiguous global version range. Keep deployment filters stable and inspect the plan before reversing selected changes.
Verify stored data, preserved schema and repeat execution on the actual target engine.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Create a disposable database, apply the migration, check the schema and rows, run to the same target again, then downgrade and verify the intended reverse. Both authoring styles use the same runner. This fragment assumes an initialized runner whose migration set creates Users.
runner.MigrateToLastVersion();
+if (!provider.TableExists("Users")) throw new Exception("Users missing");
+runner.MigrateToLastVersion();
+runner.MigrateTo(0);
+if (provider.TableExists("Users")) throw new Exception("Users was not removed");runner.MigrateToLastVersion();
+if (!provider.TableExists("Users")) throw new Exception("Users missing");
+runner.MigrateToLastVersion();
+runner.MigrateTo(0);
+if (provider.TableExists("Users")) throw new Exception("Users was not removed");Shared host · both styles
Test defaults by omitting a value, and nullability by explicitly sending NULL. Verify composite-key order, foreign-key actions and constraint names. After a SQLite rebuild check real rows, collations, supported indexes/triggers and identity high-water state. Test failure paths as well as successful SQL generation.
Use representative production-sized data to measure lock duration and backfill cost. A passing SQL-string assertion does not establish that a database accepts a command or preserves its semantics.
Build with dotnet build Migrator.slnx, then use .github/scripts/test.ps1 -Database Unit or SQLite for local suites. The live-engine guide gives the external database setup. Homepage CI results include commit provenance and skipped/missing-suite status.
Keep applied migrations immutable, review SQL and explicit reverse behavior, and serialize competing deploys. Validate against a restored database before making a breaking change. Plan application compatibility around expand/backfill/contract phases. Treat post-commit callback failures as durable migrations requiring follow-up handling.
Transaction rollback and cross-process coordination solve different problems.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
| Mode | Behavior |
|---|---|
| PerMigration | Default. Each successful migration commits independently. |
| None | Provider/operation transaction behavior; no runner-managed migration transaction. |
| WholeSession | One session transaction on SQLite, PostgreSQL or SQL Server; history initialization happens first. |
Actual atomicity depends on the database and operation. Administration commands or implicit-commit DDL can violate assumptions. AfterUp/AfterDown run after commit; WholeSession defers them until the session commit. A callback failure cannot undo durable changes.
runner.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+runner.MigrateToLastVersion();runner.Options.TransactionMode = MigrationTransactionMode.WholeSession;
+runner.MigrateToLastVersion();Shared host · both styles
DatabaseMigrationLock uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. The lease is session-owned and remains held across migration commits. This host fragment assumes a supported provider; SQLite rejects this built-in lock.
runner.Options.Lock = new DatabaseMigrationLock();
+runner.Options.LockTimeout = TimeSpan.FromSeconds(60);
+runner.MigrateToLastVersion();runner.Options.Lock = new DatabaseMigrationLock();
+runner.Options.LockTimeout = TimeSpan.FromSeconds(60);
+runner.MigrateToLastVersion();Shared host · both styles
Native locks are keyed by database, history table and scope. Coordinate separately if different scopes modify shared objects. Do not close/replace the connection, switch databases or manipulate the native lock inside a migration. MySQL named locks coordinate one server, not an entire distributed cluster.
Implement IMigrationLock for another lease mechanism, or serialize deployments outside the process. A transaction, history primary key or ordinary database write lock alone does not prove that the whole migration sequence is serialized.
Update source definitions while preserving the history your databases already contain.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Replace old ColumnProperty flags with IsNullable, IsIdentity and IsUnsigned. Primary, unique, foreign and check constraints belong to the table. GetColumns returns column attributes; use GetTableConstraints for key membership and ordered columns.
Keep the same applied migration versions and effective scope when recompiling. Do not create a new history table merely to make an incompatible source assembly run. Verify the upgrade against a restored database and a fresh database.
Column changes preserve explicit uniqueness; old SQL Server ownership markers no longer control deletion. TimeSpan inputs mean intervals, so convert clock-time inputs to TimeOnly. SQLite GUID defaults use the same blob representation as inserted parameters; unrelated rebuilds preserve existing text defaults.
Read the complete compatibility migration guide for constructor replacements, custom-provider contracts, identity, constraint metadata and collation mappings. Version-specific details live there; these chapters describe the current API.
Number changes, keep applied source immutable and give independent modules explicit histories.
Examples use Classic and Fluent tabs. Your choice follows you through the manual.
Migration accepts a numeric version or year/month/day/hour/minute/second components. Use one monotonic scheme per migration set. The date constructor builds a numeric identifier; it does not consult a clock or resolve branch collisions for you. Missing lower-numbered versions up to the target can still be applied.
using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(2026, 9, 23, 10, 0, 0)]
+public class AddUserEmail : Migration
+{
+ public override void Up() => Database.AddColumn("Users", new Column("Email", DbType.String, 320));
+ public override void Down() => Database.RemoveColumn("Users", "Email");
+}using System;
+using System.Data;
+using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
+
+[Migration(2026, 9, 23, 10, 0, 0)]
+public class AddUserEmail : FluentMigration
+{
+ public override void BuildUp(MigrationBuilder migration)
+ => migration.Create.Column("Email", "Users").AsString(320);
+ public override void BuildDown(MigrationBuilder migration)
+ => migration.Delete.Column("Email", "Users");
+}Choose one authoring style
An explicit MigrationAttribute.Scope selects that migration only for the matching provider scope. Unscoped migrations inherit the runner scope. Discovery, duplicate validation and history reads use the effective scope. Duplicate numeric versions in distinct explicit scopes are independent; physical tables are not isolated.
Set SchemaInfoTableName before any history access if you need a different table. AppliedMigrations lists recorded versions; LastAppliedMigrationVersion is nullable when history is empty. AssemblyLastMigrationVersion describes the loaded set.
A baseline can record versions whose schema it already includes. The runner rechecks active-scope history before each planned step, skipping newly covered versions and their AfterUp callbacks. Downward runs similarly skip versions removed by an earlier Down. Recording the baseline version itself does not create a duplicate.
Database.MigrationApplied(1, "billing");migration.Execute.WithProvider(provider => provider.MigrationApplied(1, "billing"));Inside Up() / BuildUp(MigrationBuilder migration)
Only record a version after establishing the schema it represents. History entries are not a substitute for verifying an existing database. Schema/history rollback follows the selected transaction mode. No migration-content checksum is stored.
DOTNETPROJECTS / MIGRATOR.NET
-- Write schema changes in C# with imperative or fluent APIs. Version them with your application. - Run them with the database provider and ORM you choose. Rebuild SQLite tables automatically from their live schema, without an ORM model. -
- - - -[Migration(1)]
-public class CreateUsers : Migration
-{
- public override void Up()
- {
- Database.AddTable("Users",
- new Column("Id", DbType.Int32) { IsNullable = false },
- new Column("Name", DbType.String, 255),
- new PrimaryKeyConstraint("PK_Users", "Id"));
- }
-
- public override void Down()
- {
- Database.RemoveTable("Users");
- }
-}
-
- SMALL API. EXPLICIT CONTROL.
-- Define tables, columns, indexes and constraints through a - transformation API or structured fluent builders. Use raw SQL when a change needs - database-specific behavior. -
-
- Number your migrations, implement Up() and
- Down(), and migrate to a chosen version. Applied
- migrations are recorded in the database.
-
- Keep module version histories in one database using named scopes. - Select each module’s migration assembly or types when you create - its runner. -
-SQLITE SCHEMA MIGRATIONS
-Migrator reads the live SQLite schema and automatically rebuilds tables for - supported column type, nullability and default changes, plus primary, foreign, - unique and check constraint changes. No ORM model is required.
-FluentMigrator leaves general column alterations and later foreign-key changes - to manual reconstruction. DbUp and Evolve execute your scripts. EF Core also - rebuilds tables, using artifacts represented in its model.
-Rebuilds retain mapped rows, named and composite keys, column collations, - supported indexes and triggers, and AUTOINCREMENT high-water state. - Foreign keys retain independent update and delete actions.
-Reconstruction rejects generated columns, STRICT and WITHOUT ROWID tables, - and indexes with explicit collations. Hidden rowid values can change; - arbitrary dependent SQL requires a migration plan.
-Compare SQLite operations, sources and preservation limits →
-SCHEMA DEFINITIONS
Named constraints, explicit defaults and provider-aware collations.
-Define primary, unique, foreign-key and check constraints as table objects. - The imperative and fluent APIs share column definitions, trusted SQL defaults and typed collation requests.
-new Column("Id", DbType.String, 27)
- { DefaultValue = RawSql.Insert("ksuid_new()") };
-
-builder.Create.Table("Events")
- .WithColumn("Id").AsString(27)
- .WithDefaultValue(RawSql.Insert("ksuid_new()"))
- .WithColumn("Name").AsString(100)
- .WithCollation(Collation.AsciiIgnoreCase);The target database must supply the SQL function. Collation mappings have explicit provider limits: - The SQLite example uses ASCII-only NOCASE; it does not satisfy a Unicode case-insensitive request. - Read the migration guide ↗.
-Use TimeOnly for time-of-day values and TimeSpan for intervals.
- Review type support, precision and data limits by provider.
Runner options support tags, named profiles, ordered maintenance and consolidated history. - Integrate migration constructors and lifecycle logging with the optional Microsoft DI package. - Explore runner and deployment options.
-Additional databases require passing real-engine CI. - SAP HANA provider qualification ↗; - Redshift, Snowflake and Db2 for IBM i remain unsupported.
-QUICK START
-
- A minimal SQLite example using the current repository API.
- Use .NET 9 and a checkout of this repository.
-
- Run these commands from your Migrator.NET checkout. This example references - the source project and passes an open SQLite connection to the provider. -
- View package versions on NuGet ↗ -dotnet new console -n MigrationDemo -f net9.0
-cd MigrationDemo
-dotnet add reference ../src/Migrator/DotNetProjects.Migrator.csproj
-dotnet add package Microsoft.Data.Sqlite --version 9.0.7
- - Add a public migration class. Each version must be unique within - the migration set loaded by a runner. -
-
- Down() is your explicit reverse operation; dropping
- a table also removes its data.
-
using System.Data;
-using DotNetProjects.Migrator.Framework;
+
+Database changes, written in C# · Migrator.NET Skip to content
+
+Migrator.NET
+
+
+
+
+
+
+ DOTNETPROJECTS / DATABASE MIGRATIONS
+ Database changes,
written in C#.
+ A table today. A different table tomorrow. Keep every change explicit, versioned, and close to your application.
+ Choose Classic or Fluent migrations. Bring your ADO.NET driver. Run the same migration system alongside any ORM—or without one.
+
+
+
+ ONE CHANGE. TWO WAYS TO WRITE IT.001 ↘
+Classic
+using System.Data;
+using DotNetProjects.Migrator.Framework;
-[Migration(1)]
-public class CreateUsers : Migration
+[Migration(1)]
+public class CreateUsers : Migration
{
- public override void Up()
+ public override void Up()
{
- Database.AddTable("Users",
- new Column("Id", DbType.Int32) { IsNullable = false },
- new Column("Name", DbType.String, 255),
- new PrimaryKeyConstraint("PK_Users", "Id"));
+ Database.AddTable("Users",
+ new Column("Id", DbType.Int32) { IsNullable = false },
+ new Column("Name", DbType.String, 255),
+ new PrimaryKeyConstraint("PK_Users", "Id"));
}
- public override void Down()
- {
- Database.RemoveTable("Users");
- }
-}
-
-
-
-
- 3
- Run pending migrations
-
- Replace Program.cs with this code, then run
- dotnet run. The runner discovers the migration in
- your assembly and records it under the default scope.
-
-
- Subsequent runs skip applied versions. Use
- MigrateTo(version) to target an earlier or later
- version.
-
-
-
-
- Program.cs
-
- using DotNetProjects.Migrator;
-using DotNetProjects.Migrator.Providers;
-using Microsoft.Data.Sqlite;
-
-using var connection = new SqliteConnection("Data Source=app.db");
-connection.Open();
-
-using var provider = ProviderFactory.Create(
- ProviderTypes.SQLite, connection, defaultSchema: null);
+ public override void Down() => Database.RemoveTable("Users");
+}
+Fluent
+using DotNetProjects.Migrator.Framework;
+using DotNetProjects.Migrator.Framework.Fluent;
-var migrator = new Migrator(
- provider, typeof(CreateUsers).Assembly, trace: false);
-
-if (migrator.LastAppliedMigrationVersion is long applied
- && applied > migrator.AssemblyLastMigrationVersion)
+[Migration(1)]
+public class CreateUsers : FluentMigration
{
- throw new InvalidOperationException(
- "Database version is newer than this application.");
-}
-
-migrator.MigrateToLastVersion();
-
-
-
- FLUENT API
-Use fluent and imperative migrations in the same assembly and runner.
-Replace CreateUsers.cs from step 2 with this class.
- Keep the source reference and Program.cs from the quick start.
BuildUp collects operations before execution;
- BuildDown describes the reverse change.
Builders cover tables, columns, keys, indexes, data and SQL. - Database support still depends on the provider.
-using DotNetProjects.Migrator.Framework;
-using DotNetProjects.Migrator.Framework.Fluent;
-
-[Migration(1)]
-public class CreateUsers : FluentMigration
-{
- public override void BuildUp(MigrationBuilder migration)
+ public override void BuildUp(MigrationBuilder migration)
{
- migration.Create.Table("Users")
- .WithColumn("Id").AsInt32().NotNullable()
- .WithPrimaryKey("PK_Users", "Id")
- .WithColumn("Name").AsString(255);
+ migration.Create.Table("Users")
+ .WithColumn("Id").AsInt32().NotNullable()
+ .WithColumn("Name").AsString(255)
+ .WithPrimaryKey("PK_Users", "Id");
}
- public override void BuildDown(MigrationBuilder migration)
- {
- migration.Delete.Table("Users");
- }
-}
- DATABASE PROVIDERS
-
- Supply your ADO.NET driver.
Migrator supplies the schema
- operations.
-
- This is an implementation inventory, not a certification of every - server or driver version. Schema operations and transactional DDL vary - by provider. Check the - provider factory - and - provider tests - for your database. -
-BUILD AND DATABASE TESTS
-The CI matrix runs unit tests and 11 database suites, then checks for missing - or duplicate test assignments. Results count test executions across the matrix; - skipped tests are shown separately.
- -Test totals are populated during deployment from the latest completed master CI run. - View CI runs and test-result artifacts ↗.
- -Choose one authoring style
01 / AUTHORWrite a numbered change.
02 / REVIEWPlan the next step.
03 / APPLYLeave a lasting record.
SMALL PRIMITIVES. REAL DATABASES.
Direct provider calls or a fluent builder. Tables, columns, indexes, constraints and data, with raw SQL when you need it.
Compare the APIs ↗Version plans, tags, profiles, maintenance stages, transaction modes and native locks. A CLI or a runner inside your own host.
Choose a runner ↗Track applied versions and give modules separate histories through scopes. Author the reverse for changes that need it.
Understand versioning ↗A PARTICULAR STRENGTH / SQLITE
Change the schema you have. Without an ORM model.
Migrator reads SQLite’s live schema and automatically reconstructs tables for supported changes to column types, defaults and nullability, and primary, foreign, unique and check constraints.
Existing rows are copied and supported schema artifacts are preserved. You describe the change; the provider handles the rebuild.
FluentMigrator requires manual reconstruction for general column alterations and later foreign-key changes. DbUp and Evolve leave it to your scripts. EF Core also rebuilds tables, using model metadata.
Read the SQLite guide and preservation limits ↗IdINTEGER · PRIMARY KEY
Name255 500 · NOT NULL
↳ Existing rows travel with the schema.
Database.ChangeColumn("Users", new Column("Name", DbType.String, 500)
+{
+ IsNullable = false, DefaultValue = "Unknown",
+ Collation = Collation.AsciiIgnoreCase
+});migration.Alter.Column("Name", "Users")
+ .AsString(500).NotNullable().WithDefaultValue("Unknown")
+ .WithCollation(Collation.AsciiIgnoreCase);Inside Up() / BuildUp(MigrationBuilder migration)
FROM EMPTY FOLDER TO FIRST TABLE
Install the library and your database driver. Add the migration above, wire up the runner, and apply it. SQLite makes a convenient first database.
Follow the complete quick start ↗dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7Shared commands · both styles
THE MIGRATION MANUAL
Detailed guides, paired examples, and provider notes.
Browse every chapter ↗
Tables, columns, data, indexes and constraints.
02 / EXECUTIONConfiguration, transactions, DI, CLI and SQL preview.
03 / DATABASESStorage behavior, SQLite rebuilds and engine differences.
04 / PRACTICEReversals, conditional changes, testing and upgrades.
BRING YOUR DATABASE
Capabilities follow the database engine. The provider guide explains aliases, drivers and operation-specific behavior.
BUILD AND DATABASE TESTS
The CI matrix runs unit tests and 11 database suites, then checks for missing or duplicate test assignments. Counts represent test executions across the matrix; skipped tests are shown separately.
+Test totals are populated during deployment from the latest completed master CI run. View CI runs and test-result artifacts ↗.
+Use fluent operations, version planning, a SQL-preview subset, runner options, native locks and the CLI. - Read the runner guide and provider limits. + Read the runner guide and provider limits.
Migrator fits applications that want explicit C# migrations and @@ -817,39 +459,8 @@
CONTINUING MIGRATOR.NET
-- DotNetProjects.Migrator continues the original Migrator.NET project, - bringing together fork contributions with work on SQLite schema - handling, provider independence and migration scopes. -
-EXPLICIT CHANGES. A LASTING RECORD.