How to automate database migrations with Fluent Migrator in Azure DevOps?
What is a Fluent Migrator?
Fluent Migrator is a migration framework for .NET that enables the creation of database migrations by using C#. This basically means that you can create manually C# class for each migration. Fluent Migrator provides a Fluent API which is a really easy interface to construct migrations. Fluent Migrator is a good combination with Dapper which itself doesn't provide any data migration functionalities.
You can find more information and documentation about Fluent Migrator from here.
This blog post shows how to use Fluent Migrator and how to automate database migrations in Azure DevOps.
How to start with Fluent Migrator?
Create a class library project and install the following Nuget packages.
# Install the FluentMigrator package to the project named Migrations.
Install-Package FluentMigrator -ProjectName Migrations
# Install the FluentMigrator.Extensions.SqlServer package to the project named Migrations.
Install-Package FluentMigrator.Extensions.SqlServer -ProjectName Migrations
FluentMigrator.Extensions.SqlServer package provides really nice extensions ex. for data seeding operations.
Example migrations
The first migration (202205060000_AddProductsTable.cs) creates a new table called "Products".
using FluentMigrator;
namespace Migrations
{
[Migration(202205060000)]
public class AddProductsTable : Migration
{
public override void Up()
{
Create.Table("Products")
.WithColumn("ProductId")
.AsInt64()
.PrimaryKey()
.Identity()
.WithColumn("ProductName")
.AsString()
.Indexed();
}
public override void Down()
{
Delete.Table("Products");
}
}
}
The second migration (202205061000_AddProductPriceField.cs) adds the Product Price field to the Products table.
using FluentMigrator;
namespace Migrations
{
[Migration(202205061000)]
public class AddProductPriceField : Migration
{
public override void Up()
{
Alter.Table("Products")
.AddColumn("ProductPrice")
.AsDecimal();
}
public override void Down()
{
Delete
.Column("ProductPrice")
.FromTable("Products");
}
}
}
As you can see Fluent API is very intuitive and easy to use!
How to execute database migrations locally?
The fluent Migrator Runner Nuget package enables execution of the migrations via C# code but I concentrate on this sample to use the Fluent Migrator .NET CLI tool.
Install Fluent Migration .NET CLI tool
dotnet tool install -g FluentMigrator.DotNet.Cli
Execute migrations to Local DB
dotnet fm migrate -p sqlserver -c "Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=Products;Connect Timeout=30;Integrated Security=SSPI;" -a "Migrations.dll"
After execution, you should see applied migrations in the VersionInfo table.
How to execute database migrations automatically in Azure DevOps?
Overview to flow
In this sample, ADO Pipeline and Database migration are executed with different service principals. Basically ADO Pipeline's service principal has access to KeyVault but it doesn't have Database permissions.
Database migrations YAML-template
This template is responsible for building Visual Studio project which contains C# based database migrations and publishing Migrations.dll artifacts to the Artifact store. The template also installs Fluent Migrator .NET CLI tool and applies pending migrations to the specific Azure SQL database.
The database connection is opened with Azure service principal (ClientId + Client Secret). When you want to open a database connection with the Service principal you should configure the Authentication parameter as "Active Directory Service Principal" and determine the "User Id" and "Password" fields in the connection string. User ID stands for Client ID of Service principal (app registration) and Password is secret of Service principal.
parameters:
- name: databaseServerName
type: string
- name: databaseName
type: string
- name: projectName
type: string
- name: azureDevOpsServicePrincipalName
type: string
- name: keyVaultResourceName
type: string
- name: keyVaultClientId
type: string
- name: keyVaultClientSecretName
type: string
jobs:
- job: DatabaseMigration
displayName: Building migrations
variables:
- name: databaseConnectionString
value: 'Data Source=${{parameters.databaseServerName}}.database.windows.net;Initial Catalog=${{parameters.databaseName}};Connect Timeout=30;Authentication=Active Directory Service Principal;User Id=${{parameters.keyVaultClientId}};Password=$(${{parameters.keyVaultClientSecretName}})'
steps:
- task: NuGetCommand@2
displayName: 'Restore solution'
inputs:
command: "restore"
feedsToUse: "config"
nugetConfigPath: $(Build.SourcesDirectory)\NuGet.Config
externalFeedCredentials: Test-DevOps
includeNuGetOrg: true
restoreSolution: "**/*.sln"
- task: MSBuild@1
displayName: "Build migrations project"
inputs:
solution: "**/${{parameters.projectName}}.csproj"
configuration: release
msbuildArguments: '/p:SkipInvalidConfigurations=false /p:OutDir="$(System.DefaultWorkingDirectory)\build"'
- task: CopyFiles@2
displayName: 'Copy migration artifact'
inputs:
sourceFolder: 'src/${{parameters.projectName}}/obj/Release'
contents: '${{parameters.projectName}}.dll'
targetFolder: $(Build.ArtifactStagingDirectory)
- task: PublishBuildArtifacts@1
displayName: 'Publish migration artifact'
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Migrations
- task: AzureCLI@2
displayName: 'Install fluent migrator .NET CLI'
inputs:
azureSubscription: '${{parameters.azureDevOpsServicePrincipalName}}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
dotnet tool install -g FluentMigrator.DotNet.Cli
- task: DownloadBuildArtifacts@0
displayName: 'Download migration artifact'
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'Migrations'
downloadPath: '$(System.ArtifactsDirectory)'
- task: AzureKeyVault@2
displayName: 'Fetch Migrator client secret from KeyVault'
inputs:
azureSubscription: '${{parameters.azureDevOpsServicePrincipalName}}'
KeyVaultName: '${{parameters.keyVaultResourceName}}'
SecretsFilter: '${{parameters.keyVaultClientSecretName}}'
RunAsPreJob: false
- task: AzureCLI@2
displayName: 'List applied and pending migrations'
inputs:
azureSubscription: '${{parameters.azureDevOpsServicePrincipalName}}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
dotnet fm list migrations -p sqlserver -c '$(databaseConnectionString)' -a "$(Build.ArtifactStagingDirectory)/Migrations/${{parameters.projectName}}.dll"
- task: AzureCLI@2
displayName: 'Apply all migrations'
inputs:
azureSubscription: '${{parameters.azureDevOpsServicePrincipalName}}'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
dotnet fm migrate -p sqlserver -c '$(databaseConnectionString)' -a "$(Build.ArtifactStagingDirectory)/Migrations/${{parameters.projectName}}.dll"
Pipeline which orchestrates deployment can then consume this template like this.
jobs:
- template: "jobs/database-migration.yaml"
parameters:
databaseServerName: productsDbServerTest
databaseName: ProductsTest
projectName: Migrations
azureDevOpsServicePrincipalName: Test-DevOps
keyVaultResourceName: TestKeyVault
keyVaultClientId: 00000000-0000-0000-0000-000000000000
keyVaultClientSecretName: DatabaseMigrator-ClientCredentials-Secret-Test
Summary
Fluent Migrator provides varied ways to execute & automate database migrations and it's very easy to use. DotNET CLI contains all the basic features that you need to automate database migrations. I'll use this definitely in the future, especially in projects where Dapper is used instead of Entity Framework.