Skip to main content

How to Generate a SQL Script from EF Core Migrations

· 8 min read
Jagdish Kumawat
Founder @ Dewiride

dotnet ef database update applies migrations straight to a database. Often you want the SQL instead: to read exactly what will run, to have a database administrator review it, or to apply it where the tools can't reach. dotnet ef migrations script generates that SQL from your migrations. This guide covers every form of the command with real scripts for MySQL, and shows why reading the SQL catches problems the C# migration hides.

Prerequisites​

The Two Arguments​

EF Core's applying migrations page defines them:

  • from: "The from migration should be the last migration applied to the database before running the script. If no migrations have been applied, specify 0 (this is the default)."
  • to: "The to migration is the last migration that will be applied to the database after running the script. This defaults to the last migration in your project."

Step 1: The Script for One Change​

The most useful form: only what one migration does. Give the migration before it as from, and the migration itself as to:

Terminal
dotnet ef migrations script CreateCatalogTables LowercaseTableNames --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output
START TRANSACTION;
ALTER TABLE `Products` DROP CONSTRAINT `FK_Products_Categories_CategoryId`;

ALTER TABLE Products RENAME products;

ALTER TABLE Categories RENAME categories;

ALTER TABLE `products` RENAME INDEX `IX_Products_CategoryId` TO `IX_products_CategoryId`;

ALTER TABLE `products` ADD CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE CASCADE;

INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20261103043533_LowercaseTableNames', '10.0.14');

COMMIT;

A terminal where dotnet ef migrations script CreateCatalogTables LowercaseTableNames prints the SQL for that one migration: drop a foreign key, rename the tables to lowercase, rename the index, add the foreign key, and record the migration in __EFMigrationsHistory.

How to read it:

  • START TRANSACTION … COMMIT wrap the migration. On MySQL that's less than it seems: every ALTER TABLE commits on its own, so a failure halfway leaves the earlier statements applied.
  • The statements are the migration's changes. Here, the C# migration also dropped and re-added the primary keys, but Oracle's provider leaves those out, because MySQL calls every primary key PRIMARY anyway. Reading the SQL is how you find such differences.
  • The INSERT INTO __EFMigrationsHistory records the migration as applied, which is how dotnet ef knows later.

Step 2: Everything After a Given Migration​

Give only from, and to defaults to the last migration. This is the script that brings a database from a known state up to date:

Terminal
dotnet ef migrations script AddStockMovements --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output
START TRANSACTION;
ALTER TABLE `stock_movements` MODIFY `Type` varchar(32) NOT NULL;

UPDATE `stock_movements`
SET `Type` = CASE `Type`
WHEN '0' THEN 'Receipt'
WHEN '1' THEN 'Adjustment'
WHEN '2' THEN 'Sale'
WHEN '3' THEN 'SaleCancelled'
ELSE `Type`
END;

INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20261103044948_StoreMovementTypeAsString', '10.0.14');

CREATE INDEX `IX_stock_movements_ProductId_OccurredAtUtc` ON `stock_movements` (`ProductId`, `OccurredAtUtc`);

DROP INDEX IX_stock_movements_ProductId ON stock_movements;

INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20261103045135_AddMovementHistoryIndex', '10.0.14');

COMMIT;

Two migrations, each followed by its history row. The UPDATE … CASE came from migrationBuilder.Sql(…) that I added to the migration by hand: scripts contain your edits exactly as written.

Which migration was last applied to the target database? Ask it: SELECT MigrationId FROM __EFMigrationsHistory ORDER BY MigrationId DESC LIMIT 1;. How to List EF Core Migrations and See Which Ones Are Applied shows more.

Step 3: All Migrations, into a File​

With no arguments, the script goes from an empty database (0) to the last migration. Add --output; the docs say "By default, the command writes the script to standard output. Use --output (or -o) to create a deployment artifact with a predictable name":

Terminal
dotnet ef migrations script --output artifacts/schema.sql --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output
Build started...
Build succeeded.

The file starts by creating the history table if it doesn't exist, then runs each migration:

artifacts/schema.sql (first lines)
CREATE TABLE IF NOT EXISTS `__EFMigrationsHistory` (
`MigrationId` varchar(150) NOT NULL,
`ProductVersion` varchar(32) NOT NULL,
PRIMARY KEY (`MigrationId`)
);

START TRANSACTION;
CREATE TABLE `Categories` (
`Id` int NOT NULL AUTO_INCREMENT,
`Name` longtext NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`)
);

This one was 127 lines for 10 migrations. Two details about the file: it starts with a UTF-8 byte order mark, which the mysql client accepted when I ran a file like it, and artifacts/ is ignored by the .NET .gitignore, a good place for generated files. To run such a file against MySQL in Docker, see How to Connect to MySQL in a Docker Container with the mysql Client.

Step 4: A Rollback Script​

The docs add: "You can use a from that is newer than the to in order to generate a rollback script." That runs the Down methods:

Terminal
dotnet ef migrations script AddMovementHistoryIndex StoreMovementTypeAsString --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output
START TRANSACTION;
CREATE INDEX `IX_stock_movements_ProductId` ON `stock_movements` (`ProductId`);

DROP INDEX IX_stock_movements_ProductId_OccurredAtUtc ON stock_movements;

DELETE FROM `__EFMigrationsHistory`
WHERE `MigrationId` = '20261103045135_AddMovementHistoryIndex';

COMMIT;

A terminal where a rollback script from AddMovementHistoryIndex back to StoreMovementTypeAsString recreates the single-column index, drops the two-column index and deletes the migration's row from __EFMigrationsHistory.

The history row is deleted instead of inserted. Generate the rollback script together with the deployment script, and you know exactly how to undo a release before you need to.

About --idempotent​

An idempotent script checks the history table and applies only what's missing, so it can run against any database. The docs warn that "Idempotent script support depends on the database provider." With Oracle's MySQL provider in EF Core 10, the generated script uses SQL Server syntax that MySQL rejects with ERROR 1064. That problem has its own post, with the from/to workaround from Step 2.

Common Mistakes​

MistakeWhat happensDo this instead
Running a full script on a database that already has migrationsCREATE TABLE fails: the tables existScript from the last applied migration
Guessing the database's last migrationStatements run twice, or are skippedRead __EFMigrationsHistory
Trusting the C# migration instead of the SQLProvider differences go unnoticedRead the script before applying it
Expecting the transaction to undo a failed script on MySQLEarlier ALTER TABLEs stay appliedCheck the table after any failure

FAQ​

Can I name migrations by Id instead of name?​

Yes. Both LowercaseTableNames and 20261103043533_LowercaseTableNames work.

Does generating a script connect to the database?​

No. It reads only your migrations, which is why it works anywhere, even without the database.

Conclusion​

dotnet ef migrations script [from] [to] turns migrations into SQL: one migration with both arguments, everything after a migration with only from, the whole history with none, a rollback when from is newer than to, and a file with --output. Read the script before applying it: it shows what the provider really sends to MySQL, including your own edits.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.