DbUp vs FluentMigrator: Two Ways to Migrate .NET Databases
DbUp runs plain SQL scripts once and journals them; FluentMigrator describes changes in C#. How each works, where each wins, and how both sit next to EF Core.
C# migration classes with a fluent API, numbered by the [Migration] attribute. SchemaDrawer runs FluentMigrator 8.0.1 on .NET 10 against a throw-away database of the engine you choose and returns an SVG of the schema it produces.
Upload the C# migration classes, at any depth. Each class derives from Migration and carries a [Migration(version)] attribute. The files are compiled at run time against FluentMigrator and its PostgreSQL, MySQL and Oracle extension packages; no other NuGet package is available.
The sample below is the PetClinic schema written for FluentMigrator, one of the sets SchemaDrawer's own tests draw on every engine:
Migrations/ ├── 20240101000001_CreateOwnersAndPets.cs ├── 20240101000002_CreateVets.cs └── 20240101000003_CreateVisits.cs
C# source files.
dotnet build of a project holding your files, then MigrateUp() through the FluentMigrator runner. The database is a fresh PostgreSQL, MySQL, MariaDB, SQL Server, Oracle or SQLite container in the version you ask for; nothing of yours is touched.
PostgreSQL 10, 11, 12, 13, 14, 15, 16, 17, 18 MySQL 8.0, 8.4, 9 MariaDB 10.6, 11.4, 11.8, 12 SQL Server 2017, 2019, 2022, 2025 Oracle 21, 23 SQLite
Not offered for FluentMigrator: SQLite. The tool ships no driver for it, so nothing could run.
migrationType=FLUENT_MIGRATOROwners, pets, types, vets, specialties and visits: the same seven tables every sample set on this site creates. These are the exact files the tests upload.
using FluentMigrator;
namespace Migrations;
[Migration(20240101000001)]
public class CreateOwnersAndPets : Migration
{
public override void Up()
{
Create.Table("owners")
.WithColumn("id").AsInt32().NotNullable().PrimaryKey("pk_owners")
.WithColumn("first_name").AsString(50).NotNullable()
.WithColumn("last_name").AsString(50).NotNullable()
.WithColumn("address").AsString(255).Nullable()
.WithColumn("city").AsString(80).Nullable()
.WithColumn("telephone").AsString(20).Nullable();
Create.Table("types")
.WithColumn("id").AsInt32().NotNullable().PrimaryKey("pk_types")
.WithColumn("name").AsString(80).NotNullable();
Create.Table("pets")
.WithColumn("id").AsInt32().NotNullable().PrimaryKey("pk_pets")
.WithColumn("name").AsString(50).NotNullable()
.WithColumn("birth_date").AsDate().Nullable()
.WithColumn("type_id").AsInt32().NotNullable().ForeignKey("fk_pets_types", "types", "id")
.WithColumn("owner_id").AsInt32().NotNullable().ForeignKey("fk_pets_owners", "owners", "id");
}
public override void Down()
{
Delete.Table("pets");
Delete.Table("types");
Delete.Table("owners");
}
}
using FluentMigrator;
namespace Migrations;
[Migration(20240101000002)]
public class CreateVets : Migration
{
public override void Up()
{
Create.Table("vets")
.WithColumn("id").AsInt32().NotNullable().PrimaryKey("pk_vets")
.WithColumn("first_name").AsString(50).NotNullable()
.WithColumn("last_name").AsString(50).NotNullable();
Create.Table("specialties")
.WithColumn("id").AsInt32().NotNullable().PrimaryKey("pk_specialties")
.WithColumn("name").AsString(80).NotNullable();
// The composite key and both foreign keys are declared inside the table rather than added
// afterwards. SQLite cannot add either to a table that already exists, and FluentMigrator
// does not refuse in advance - it writes the ALTER TABLE and hands back SQLite's complaint.
// Named inline, the two columns make one composite key, and every engine takes it.
Create.Table("vet_specialties")
.WithColumn("vet_id").AsInt32().NotNullable().PrimaryKey("pk_vet_specialties")
.ForeignKey("fk_vet_specialties_vets", "vets", "id")
.WithColumn("specialty_id").AsInt32().NotNullable().PrimaryKey("pk_vet_specialties")
.ForeignKey("fk_vet_specialties_specialties", "specialties", "id");
}
public override void Down()
{
Delete.Table("vet_specialties");
Delete.Table("specialties");
Delete.Table("vets");
}
}
using FluentMigrator;
namespace Migrations;
[Migration(20240101000003)]
public class CreateVisits : Migration
{
public override void Up()
{
Create.Table("visits")
.WithColumn("id").AsInt32().NotNullable().PrimaryKey("pk_visits")
.WithColumn("pet_id").AsInt32().NotNullable().ForeignKey("fk_visits_pets", "pets", "id")
.WithColumn("visit_date").AsDate().Nullable()
.WithColumn("description").AsString(255).Nullable();
}
public override void Down()
{
Delete.Table("visits");
}
}
Every variant of the sample produces these seven tables. This diagram was drawn from the Flyway variant on PostgreSQL 18; on MySQL the types read INT and VARCHAR, on Oracle the names are upper case.
One multipart request queues the drawing; poll the job until it succeeds and download the SVG. Or let the GitHub Action do it and open a pull request with the diagram.
curl -X POST https://schemadrawer.com/api/v1/drawing \
-H "Request-Id: $(uuidgen)" \
-F "userId=5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f" \
-F "migrationType=FLUENT_MIGRATOR" \
-F "databaseType=POSTGRESQL" \
-F "version=18" \
-F "files=@Migrations/20240101000001_CreateOwnersAndPets.cs" \
-F "files=@Migrations/20240101000002_CreateVets.cs" \
-F "files=@Migrations/20240101000003_CreateVisits.cs"
# {"jobId":"0b6e2f4c-8d1a-4c3e-9f7a-2a5b6c7d8e9f"}
Then poll GET https://schemadrawer.com/api/v1/drawing/{jobId} until status is SUCCESS and fetch diagramUrl. Details in the getting started guide and the API reference.
- uses: schemadrawercom/action@v1
with:
sql-directory: Migrations
output-path: docs/schema-diagram.svg
migration-type: FLUENT_MIGRATOR
database-type: POSTGRESQL
database-version: '18'
github-token: ${{ secrets.GITHUB_TOKEN }}
The action uploads the directory, waits for the diagram and opens a pull request with the SVG. See the action reference.
VersionInfo table is left out of the diagram.DbUp runs plain SQL scripts once and journals them; FluentMigrator describes changes in C#. How each works, where each wins, and how both sit next to EF Core.
Editing applied migrations, environment-specific scripts, ORM auto-DDL in production, untested rollbacks: ten habits that break deployments, with a fix for each.
How Diesel manages a schema in Rust: the CLI, timestamped folders with up.sql and down.sql, the versions table, the generated schema.rs and embedded migrations.
Upload the migration directory, get an SVG back. Free while we build it.