Working with Flyway and Entity Framework Code First: Automation
This article will demonstrate how to automate a hybrid database change management system that uses Entity Framework Code First for development and Flyway for deployments. We automatically convert C# migrations, produced by EF, to the Flyway format and then use Flyway command line to deploy the migrations and save the 'object-level state' of each new database version, so we can track exactly which objects changed, and how, between versions.
Entity Framework Code First is a fixture of the database development process for many .NET development teams. Of course, the premise is an attractive one: the developers just adapt the data model, in code, while a tool automatically keeps track of the database schema changes and generates migration scripts.
However, as discussed in my previous article, Working with Flyway and Entity Framework Code First: An Overview, there are downsides too, around a lack of visibility into changes, often lack of testing, a tendency to produce complex, poorly performing SQL, all of which can lead to trouble at deployment time.
I went on to propose a ‘hybrid’ Entity Framework-Flyway model where an EF-managed development process still produces the database migration scripts, but we use Flyway to increase the visibility and reliability of the deployment process, making it much easier to automate. The last article was the overview and now we get to implementation. I’ll show you the basics of how to automate each part of the EF-Flyway hybrid process using PowerShell and Azure DevOps pipelines.
Automating The EF-Flyway hybrid model
In the primary EF-Flyway hybrid model, we convert C#-based EF Core migrations to Flyway SQL migration scripts, and then apply those scripts to a Flyway-controlled database (the Flyway shadow database). At this point, we can use Flyway Desktop to extract and save a ‘schema model’ for the database, which we can commit to the version control repository. This automation process requires the Enterprise edition of Flyway.
What is the schema model?
The schema model is a set of object-level-scripts, organized by object type, in which every database object is matched by a script that creates it. This model describes the current state of each object in the database so by tracking changes to it, the team can see quickly which objects changed between versions, and how.
This workflow of generating the migration script first and then retrospectively saving the schema model changes is an ‘inversion’ of the usual Flyway Desktop model (where we usually save the schema model changes and then generate the migration), but it works well for us here and is easy to automate:
Step 1: Develop C#
The .NET developers update the database model directly from their C# classes. I’ve provided a sample EF Core project on GitHub.
Step 2: Script and convert SQL migrations.
Every time the .NET developer commits and pushes a C# migration, we want to export it and convert it to a Flyway SQL migration script. To automate this C#-to-Flyway script conversion process, we need a PowerShell script that will:
- Export the C# migrations to SQL, using the EF Core CLI migrations script command
- Convert the scripts to the Flyway migration format – the EF “Up” script becomes a Flyway versioned (V) migration script and the EF “Down” script becomes a Flyway versioned undo (U) script
First, we specify the paths to the EF Core and Flyway migration files and then get a list of the EF Core migration files:
1 2 3 4 5 |
$efCore = "C:\work\EFCoreFWD\EFCore" $efCoreMigrationFilesPath = "C:\work\EFCoreFWD\EFCore\Migrations\" $flywayProjectPath = "C:\work\EFCoreFWD\Flyway\" $fwdMigrationFilesPath = Join-Path $flywayProjectPath "migrations" $efCoreMigrationFiles = Get-ChildItem $efCoreMigrationFilesPath |
Before we attempt to convert any EF migration files to Flyway migrations, we check that it hasn’t already been done. We collect all Flyway migrations, chop off the flyway version number and then check for name matches against EF Core migrations:
1 2 |
$migrationfiles = Get-ChildItem $fwdMigrationFilesPath | Select-Object Name | Where-Object { $_.Name -match '(^V)' } $migrationfilesWithoutNumber = $migrationfiles | Select-Object Name | ForEach-Object { $_.Name.Substring(5, $_.Name.Length - 5).replace('.sql', '') } |
When there are existing Flyway migration files, the last matching file name indicates the current Flyway version and is our starting point. When no file is found, the conversion starts from scratch.
1 2 3 4 5 6 7 8 9 10 |
$lastMigrationFile = $migrationfiles.Name | Select-Object -last 1 if ($null -eq $lastMigrationFile) { Write-Host "Starting migration, no flyway migration found" [int]$iteration = 0 $migrationfilesWithoutNumber = @() } else { Write-Host "Starting migration, last flyway migration was $lastMigrationFile" [int]$iteration = $lastMigrationFile.Substring(1, 3) } |
For each EF Core migration that we need to convert, the script will perform the following steps:
- Check that the file is not a Model Snapshot or Designer file. We are only interested in the C# migrations.
- Skip already converted migrations but do log them to the console.
- Convert the “Up” EF Core migration file to a Flyway versioned (V) migration file and save it to disk
- The flyway version number will be determined by the iteration number.
- With regular expressions:
- Add an extra column (
FlywayInstallRank
) to the EF migration history table:
- Add an extra column (
-
-
- Remove non-SQL output from the EF Core tool output.
-
- Convert the “Down” EF Core migration file to a Flyway undo (U) file and save to disk.
- Update the previous migration file name for the next iteration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
## Loop through each EF Core migration file $PreviousEfCoreMigrationFileBaseName = "0" $undoMigrationStarted = $false # Set the working directory to the project directory for the dotnet call Set-Location $efCore foreach ($efCoreMigrationFile in $efCoreMigrationFiles) { if ($efCoreMigrationFile.BaseName -notlike '*ModelSnapshot' -and $efCoreMigrationFile.BaseName -notlike '*designer') { $CurrentEfCoreMigrationFileBaseName = $efCoreMigrationFile.BaseName # Create a Flyway migration filename Write-Host "Converting $CurrentEfCoreMigrationFileBaseName" $migrationName = $CurrentEfCoreMigrationFileBaseName.Replace("_", "__") # skip all files that are already converted to Flyway if ($migrationfilesWithoutNumber.Contains($migrationName)) { Write-Host "SKIPPED conversion" -ForegroundColor "Yellow" Write-Host "$CurrentEfCoreMigrationFileBaseName already part of the Flyway migration folder" } # converts the EF Core migration file to the Flyway format else { $migrationType = "V" # first the V(ersion) migration $iteration += 1 $version = ([string]$iteration).PadLeft(3, '0') Write-Host "Flyway version = $version" while ($undoMigrationStarted -eq $false) { # this will loop twice, for the MigrationType V and U Write-Host "Extracting the $migrationType SQL-script from EF Core for $PreviousEfCoreMigrationFileBaseName vs $CurrentEfCoreMigrationFileBaseName" if ($migrationType -eq "V") { $fileContent = dotnet ef migrations script $PreviousEfCoreMigrationFileBaseName $CurrentEfCoreMigrationFileBaseName } else { $fileContent = dotnet ef migrations script $CurrentEfCoreMigrationFileBaseName $PreviousEfCoreMigrationFileBaseName $undoMigrationStarted = $true } Write-Host "Trying to flywayify the $migrationType for: $CurrentEfCoreMigrationFileBaseName" $flywayFileContent = $fileContent | Foreach-Object { $_ ` -replace [regex]::escape('[ProductVersion] nvarchar(32) NOT NULL,'), '[ProductVersion] nvarchar(32) NOT NULL, [FlywayInstallRank] INT NULL' ` -replace [regex]::escape('Build started...'), '' ` -replace [regex]::escape('Build succeeded.'), '' } $path = ("{0}\{1}{2}_{3}.sql" -f $fwMigrationFilesPath, $migrationType, $version, $migrationName) Set-Content -Path $path -Value $flywayFileContent Write-Host "COMPLETED $migrationType conversion to $path" -ForegroundColor "Green" $migrationType = "U" # next create the (U)ndo migration } } Write-Host "..." -ForegroundColor "DarkBlue" # finishing up this iteration $PreviousEfCoreMigrationFileBaseName = $CurrentEfCoreMigrationFileBaseName $undoMigrationStarted = $false } } |
If we run the script above on the repository that is available on GitHub, the script will transform the three EF Core migrations to six Flyway migrations SQL files – three V scripts and three U scripts:
Running the script, a second time won’t do anything else than informing the user that the available migrations are already converted. The result on disk of this action is that the files in the red square are created:
That is nice, but we are now only halfway; it’s time for the next part of the script.
Step 3. Object-level versioning (schema model)
This stage marks the start of the Flyway-managed process. Our script will use the Flyway CLI to a) apply the converted migrations to the shadow database, and then b) use the Flyway Desktop CLI (flyway-dev
) to extract the schema model and save it to disk.
What is flyway-dev?
I explain the flyway-dev
CLI in another article, The Flyway Desktop CLI: Automate your Database Development Process. However, be aware that flyway-dev
is currently available in preview, for users to run trials and provide feedback. At some point, Redgate will release a fully supported command line with similar capabilities but until then the command syntax is subject to change. Please note also that this part of the demo currently only works with older versions of Flyway Desktop prior to v6.5.0, which still use the .json and .conf configuration files rather than the new unified .toml format.
a. Migrate the shadow database
This section of the script starts by retrieving the URL for the shadow database from the flyway-dev.user.json
project file. The shadow database is a feature of Flyway Enterprise, used to generate and verify migrations. We use this URL, along with the locations of the Flyway project and the current working directory (which we already set in the previous section), to define the parameters for the Flyway migrate
command, and then execute it:
1 2 3 4 |
$flywayDevUser = get-content "$flywayProjectPath/flyway-dev.user.json" | ConvertFrom-Json $url = $flywayDevUser.deployment.shadowDatabase.connectionProvider.url $params = @("migrate", "-configFiles=flyway.conf", "-workingDirectory=$flywayProjectPath", "-url=$url") flyway @params |
The shadow database now reflects the current ‘state’ of the data model, as defined in EF, and has a Flyway version number (V003, in this example). The eagle-eyed among you may have spotted, in the previous screenshot showing the newly converted Flyway migrations, an afterMigrate.sql script. This is a Flyway callback script, which runs on successful completion of a Flyway migration run and, in this case, inserts the new flyway version number into the FlywayInstallRank
column that we added earlier to the EF schema history table.
This ability to sync the EF and Flyway migration history tables can be useful when applying undo migrations and keeping track of what Flyway migration is linked to which EF Core migration.
b. Save the schema model
We now want to capture the schema model for this version of the database. We extract this from the shadow database (at V003) using the diff
command in flyway-dev
, with the migrations folder (which Flyway builds in the shadow database) as source and the schema model folder, on disk, as target. The FWD CLI will use the SQL Compare CLI to compare the two and saves the differences in a diff artifact, identified by a unique GUID. By default, this artifact will be saved to use the user’s temporary directory – in a specific FWD folder, so we stick with that. We then take
the differences from the diff artifact and apply
them to the schema model folder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$tempFilePath = Join-Path $env:LOCALAPPDATA "Temp\Redgate\Flyway Desktop\comparison_artifacts_Migrations_SchemaModel" $diffArtifactFileName = New-Guid $null = New-Item -ItemType Directory -Force -Path $tempFilePath $diffArtifactFilePath = Join-Path $tempFilePath $diffArtifactFileName $commonParams = @("--artifact=$diffArtifactFilePath", "--project=$flywayProjectPath", "--i-agree-to-the-eula") $diffParams = @("diff", "--from=Migrations", "--to=Schemamodel") + $commonParams $takeParams = @("take") + $commonParams $applyParams = @("apply") + $commonParams flyway-dev @diffParams flyway-dev @takeParams | flyway-dev @applyParamsRemove-Item $diffArtifactFilePath |
Running this script produces a lot of output, as shown in this video:
The result is that we extracted the schema model changes of the EF Core migrations, via the shadow database, to disk:
In order to ensure that we don’t accidentally include the __EFMigrationsHistory
table in the schema model, we filter it out of the comparison using the SQL Compare filter file, which you can find in the root of the Flyway Desktop project folder:
Step 4: Commit changes to Git
The final step is to commit these changes, the schema model and the flyway migrations, to the git repository. The migrations can be used to migrate various upstream environments, but only after an approval of the changes that are in the schema model, which can be reviewed by a colleague (DBA, database engineer, etc).
This review step can take place in the Pull Request. This formal administrative step will contain:
- The migrations (converted).
- The schema model changes.
- The why, via a commit message, PR comments or a linked work item (optional)
Of course, we need to tackle the question of what happens if the changes aren’t approved, for example if the reviewer finds that the way the changes were implemented will cause performance or security issues. What’s the workflow at that point? I’ll tackle this issue in the next article!
An alternative hybrid model with Flyway-generated migrations
In my previous article, I also suggested an alternate approach where, instead of converting the EF-generated migrations, we use them to update a flyway-managed build database, at the current production version. We then capture the changes to the schema model and let Flyway Desktop generate the equivalent V and U migrations.
This workflow is more akin at a conventional Flyway Desktop workflow, as described in Flyway Desktop in Database Development Work: An Overview. However, can we still automate it?
We can! I’ve provided a small script in the GitHub repo to allow the EF Core tool to run the dotnet
ef
database
update
command against the Flyway-managed Build DB. After that, the Flyway Desktop CLI (flyway-dev
) takes over. We use it to automatically save the schema model and generate the migration scripts, in response to each update of the Flyway-managed build database. I demonstrate all the flyway-dev
steps in The Flyway Desktop CLI: Automate your Database Development Process.
Once the schema model is saved and the migration files regenerated, in Flyway, we commit all the changes, which will be reviewed in a Pull Request as before.
Conclusion
Both the “EF script conversion” and “Flyway script regeneration” workflows have their pros and cons. I’d personally choose the conversion model, for a simple example like this, but with more complex code, or using an RDBMS than SQL Server, conversion problems could arise. The choice of workflow depends on the use case.
However, regardless of the chosen workflow, the EF-Flyway hybrid model provides improved visibility into database changes, earlier detection of potential issues, and better collaboration between development and database teams. It allows developers to work with the ease and familiarity of C# code during development, while relying simple, versioned SQL migrations provided by Flyway for automated deployments.
Tools in this post
Flyway Teams
Ideal for organizations looking to improve collaboration and fine tune their processes during development and the deployment of database changes.