Is it possible to generate idempotent scripts from EF Core migrations? Automatically? Yes, it is! But first, let's see what idempotency means.
A subroutine with side effects is idempotent if the system state remains the same after one or several calls, in other words, if the function from the system state space to itself associated to the subroutine is idempotent in the mathematical sense given in the definition;
Generally speaking, an idempotent script, no matter if it is written in PowerShell, bash or SQL on any other scripting language, will give you the luxury to run it over and over again, without the fear of breaking something. More than that, any script that might fall under unsupervised hands/system should have some 'safety' measures built-in.
Now, we know from HTTP concepts that GET and HEAD requests are idempotent and safe. But POST requests aren't neither safe nor idempotent.
The question is why? Well, first of all, by sending a POST request you end up creating new resources, and if you don't have a business rule to prevent requests with the same request body, you'll just have a lot of duplicates and nothing stopping you from creating them.
Why can't we apply the same rules when writing database scripts?
You must write it in such a way that you check at least the most likely scenarios.
For example:
So on and so forth, but you get the point. You need to cover all the scenarios that can end up corrupting your database.
There are several options to manage your database scripts. With so many third-party or out-of-the-box tools, sometimes is difficult to even choose.
A few years back I remember using PostgreSQL as a database for a project, and I really enjoyed the syntax. We used to manually make idempotent scripts, tested them locally, then added them to a different repository and shipped them to production with Flywayy.
Assuming you already have a migration added there are a few commands that are handy and with a few flags
dotnet ef migrations script {lastAppliedMigration} --startup-project MyApp --idempotent
dotnet ef migrations script {lastAppliedMigration} --idempotent --output {FolderName}/{sqlscriptname}.sql --startup-project MyApp
No matter the script you are running you can always add the --verbose
flag to see what is executed with that script.
Real-life example: generating a script that adds Addresses to Person
dotnet ef migrations script MadeRequired --idempotent --output Scripts/Addresses.sql --startup-project Persons
Will generate this:
IF NOT EXISTS(SELECT \* FROM \[\_\_EFMigrationsHistory\] WHERE \[MigrationId\] = N'20200314111307\_Addresses')
BEGIN
CREATE TABLE \[Address\] (
\[Id\] int NOT NULL IDENTITY,
\[StreetName\] nvarchar(max) NULL,
\[ZipCode\] nvarchar(max) NULL,
\[City\] nvarchar(max) NULL,
\[PersonId\] int NOT NULL,
CONSTRAINT \[PK\_Address\] PRIMARY KEY (\[Id\]),
CONSTRAINT \[FK\_Address\_Persons\_PersonId\] FOREIGN KEY (\[PersonId\]) REFERENCES [Persons]([Id]) ON DELETE CASCADE
);
END;
GO
IF NOT EXISTS(SELECT \* FROM \[\_\_EFMigrationsHistory\] WHERE \[MigrationId\] = N'20200314111307\_Addresses')
BEGIN
CREATE INDEX \[IX\_Address\_PersonId\] ON [Address]([PersonId]);
END;
GO
IF NOT EXISTS(SELECT \* FROM \[\_\_EFMigrationsHistory\] WHERE \[MigrationId\] = N'20200314111307\_Addresses')
BEGIN
INSERT INTO \[\_\_EFMigrationsHistory\] (\[MigrationId\], \[ProductVersion\])
VALUES (N'20200314111307\_Addresses', N'3.1.2');
END;
GO
As you can see, this script relies heavily on the __EFMigrationsHistory table, which is not exactly very...idempotent. So, my recommendation is to have another look at the generated scripts and customize and test them.
By using the right tools and knowing the right flags we can make our lives easier.
Hi! Welcome to my blog