Here is a comprehensive end-to-end guide on using DbUp in a .NET API project for schema migration, including a rollback strategy and multiple iteration examples.
DbUp is a database migration tool that allows developers to manage schema changes in a controlled and automated manner. This document provides a detailed guide on how to use DbUp in a .NET API project for managing PostgreSQL database migrations, including its advantages and disadvantages.
For more information, visit the official DbUp documentation: DbUp Documentation
To use DbUp for PostgreSQL or SQL Server, install the appropriate NuGet package:
For PostgreSQL:
dotnet add package DbUp.PostgresqlFor SQL Server:
dotnet add package DbUp.SqlServerNote: All migration, rollback, and seed data scripts must be idempotent to ensure safe re-execution without causing conflicts or duplication.
In the provided code, the DatabaseMigrationInitFilter class implements IStartupFilter, ensuring that database migrations are executed during application startup.
-
Checking Migration Status:
- The migration process begins if
EnableDatabaseMigrationis set totruein the application settings. - Migration scripts are loaded from the
DbUpMigration/Migrationsdirectory.
- The migration process begins if
-
Ensuring Database Existence:
EnsureDatabase.For.PostgresqlDatabase(dbConnectionString);ensures that the PostgreSQL database exists before applying migrations.
-
Advisory Lock Mechanism:
- Implements
pg_try_advisory_lock()to ensure that only one instance applies migrations in a concurrent environment. - The lock is held for up to 60 seconds while retrying every 5 seconds.
- Implements
-
Executing Migrations:
- Uses
DeployChanges.To.PostgresqlDatabase(dbConnectionString)to apply scripts. - Runs migration scripts only if required.
- Logs migration progress using
DbUpLoggerExtension.
- Uses
-
Seeding Data:
- Seed scripts are executed from the
DbUpMigration/Seeds/<Environment>directory. - Seed execution follows a similar approach to migrations.
- Seed scripts are executed from the
-
Transaction Support:
- Migrations and seeding are executed within transactions to maintain atomicity.
using DbUp;
using Microsoft.Extensions.Options;
using Npgsql;
using PBR.Core.Models;
using PBR.Util.Logging;
using PBR.Util.Models;
using System.Data;
using System.Runtime.ExceptionServices;
namespace PBR.Api.Filters
{
public class DatabaseMigrationInitFilter : IStartupFilter
{
private readonly IOptionsMonitor<AppSettingModel> _appSettingModel;
private readonly IOptionsMonitor<DbConnectionModel> _dbConnectionModel;
private readonly ILogger<DatabaseMigrationInitFilter> _logger;
public DatabaseMigrationInitFilter(IOptionsMonitor<AppSettingModel> appSettingModel,
IOptionsMonitor<DbConnectionModel> dbConnectionModel,
ILogger<DatabaseMigrationInitFilter> logger)
{
_appSettingModel = appSettingModel ?? throw new ArgumentNullException(nameof(appSettingModel));
_dbConnectionModel = dbConnectionModel ?? throw new ArgumentNullException(nameof(dbConnectionModel));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
if (_appSettingModel.CurrentValue.EnableDatabaseMigration)
{
_logger.LogInformationExtension("Database migration is enabled; starting the migration process.");
var dbConnectionString = DbConnectionModel.CreateConnectionString(_dbConnectionModel.CurrentValue.Read);
var dbUpLogger = new DbUpLoggerExtension(_logger);
string migrationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DbUpMigration", "Migrations");
if (Directory.Exists(migrationPath) && Directory.EnumerateFiles(migrationPath).Any())
{
// Ensure Database exists, if not then create it
EnsureDatabase.For.PostgresqlDatabase(dbConnectionString, dbUpLogger);
using var dbLockConnection = new NpgsqlConnection(dbConnectionString);
dbLockConnection.Open();
// Attempt to acquire the advisory lock with a timeout (Ensures only one process executes the DbUp migration if multiple processes are running simultaneously)
bool lockAcquired = false;
var startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalSeconds < 60) // Set a 60 seconds timeout for waiting for the lock
{
using var lockCommand = new NpgsqlCommand($"SELECT pg_try_advisory_lock({_appSettingModel.CurrentValue.PostgresqlAdvisoryLockKey});", dbLockConnection);
var result = (bool)lockCommand.ExecuteScalar();
if (result)
{
lockAcquired = true;
break;
}
// Wait for 5 second before retrying
Thread.Sleep(5000);
}
if (lockAcquired)
{
try
{
var migrationUpgrader = DeployChanges.To
.PostgresqlDatabase(dbConnectionString)
.WithScriptsFromFileSystem(migrationPath)
.WithTransaction()
.WithExecutionTimeout(TimeSpan.FromMinutes(10))
.LogTo(dbUpLogger)
.Build();
if (!migrationUpgrader.IsUpgradeRequired())
{
_logger.LogInformationExtension("No pending migrations detected; skipping the migration process.");
// Run Data Seed Scripts
SeedData(dbConnectionString);
}
else
{
_logger.LogInformationExtension("New database migrations found; initiating the migration process.");
// Run Migration Scripts
var operation = migrationUpgrader.PerformUpgrade();
if (operation.Successful)
{
_logger.LogInformationExtension("Database migration has been successfully completed.");
// Run Data Seed Scripts Only if Migration Succeeds
SeedData(dbConnectionString);
}
else
{
_logger.LogErrorExtension($"Database migration has failed. Error Message: {operation.Error.Message}", operation.Error);
CleanupResources(dbLockConnection);
// Immediately terminate the API if database migration fails to ensure previous stable API keeps running in kubernetes with a valid state
Environment.Exit(1);
}
}
}
catch (Exception ex)
{
_logger.LogErrorExtension("An exception occurred during the database migration process.", ex);
CleanupResources(dbLockConnection);
// Immediately terminate the API if database migration fails to ensure previous stable API keeps running in kubernetes with a valid state
Environment.Exit(1);
}
finally
{
CleanupResources(dbLockConnection);
}
}
}
else
{
_logger.LogInformationExtension("No database migration script found; skipping the migration process.");
}
}
return next;
}
private void SeedData(string dbConnectionString)
{
if (!string.IsNullOrEmpty(_appSettingModel.CurrentValue.Environment))
{
_logger.LogInformationExtension($"Seeding data for environment: {_appSettingModel.CurrentValue.Environment}");
var dbUpLogger = new DbUpLoggerExtension(_logger);
string seedPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DbUpMigration", "Seeds", _appSettingModel.CurrentValue.Environment);
if (Directory.Exists(seedPath) && Directory.EnumerateFiles(seedPath).Any())
{
var seedUpgrader = DeployChanges.To
.PostgresqlDatabase(dbConnectionString)
.WithScriptsFromFileSystem(seedPath)
.WithTransaction()
.WithExecutionTimeout(TimeSpan.FromMinutes(10))
.LogTo(dbUpLogger)
.Build();
if (!seedUpgrader.IsUpgradeRequired())
{
_logger.LogInformationExtension("No new seed data script found; skipping the seeding process.");
}
else
{
var seedResult = seedUpgrader.PerformUpgrade();
if (seedResult.Successful)
{
_logger.LogInformationExtension($"Data seeding for {_appSettingModel.CurrentValue.Environment} completed successfully!");
}
else
{
_logger.LogErrorExtension($"An error occurred during data seeding for {_appSettingModel.CurrentValue.Environment}. Error Message: {seedResult.Error.Message}", seedResult.Error);
ExceptionDispatchInfo.Capture(seedResult.Error).Throw();
}
}
}
else
{
_logger.LogInformationExtension("No seed data script found; skipping the seeding process.");
}
}
}
private void CleanupResources(NpgsqlConnection dbLockConnection)
{
if (dbLockConnection != null && dbLockConnection.State == ConnectionState.Open)
{
// Release the advisory lock after migrations/seeds are done
using var unlockCommand = new NpgsqlCommand($"SELECT pg_advisory_unlock({_appSettingModel.CurrentValue.PostgresqlAdvisoryLockKey});", dbLockConnection);
unlockCommand.ExecuteNonQuery();
dbLockConnection.Close();
}
}
}
}
using DbUp.Engine.Output;
using PBR.Util.Logging;
public class DbUpLoggerExtension : IUpgradeLog
{
public readonly ILogger _logger;
public DbUpLoggerExtension(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void LogTrace(string format, params object[] args)
{
_logger.LogTraceExtension(string.Format(format, args));
}
public void LogDebug(string format, params object[] args)
{
_logger.LogDebugExtension(string.Format(format, args));
}
public void LogInformation(string format, params object[] args)
{
_logger.LogInformationExtension(string.Format(format, args));
}
public void LogWarning(string format, params object[] args)
{
_logger.LogWarningExtension(string.Format(format, args));
}
public void LogError(string format, params object[] args)
{
_logger.LogErrorExtension(string.Format(format, args), null);
}
public void LogError(Exception ex, string format, params object[] args)
{
_logger.LogErrorExtension(string.Format(format, args), ex);
}
}// Add DbUp Migration
services.AddTransient<IStartupFilter, DatabaseMigrationInitFilter>();This ensures that DatabaseMigrationInitFilter runs during application startup.
DbUpMigration
├── Migrations (Contains migration scripts)
│ ├── 20250212-111700-PBR-34-Initial-Schema.sql
│ ├── 20250213-103000-PBR-35-Move-Detarment-To-Workspace.sql
├── Rollbacks (Contains rollback scripts)
├── Seeds (Contains environment-specific seed data)
│ ├── Dev
│ │ ├── 20250224-013700-PBR-34-Default-PowerBi-Account.sql
│ ├── QA
│ ├── UAT
DbUp ensures that database migrations execute sequentially while preventing duplicate execution of already applied scripts. The execution process follows these steps:
-
Initialization and Configuration:
- The
DatabaseMigrationInitFilterchecks ifEnableDatabaseMigrationis set totruein the application settings. - The database connection string is retrieved.
- The migration scripts directory is verified.
- The
-
Ensuring Database Existence:
- The database is created if it does not exist using
EnsureDatabase.For.PostgresqlDatabase(dbConnectionString);.
- The database is created if it does not exist using
-
Advisory Lock for Concurrency Handling:
- The process attempts to acquire an advisory lock to prevent multiple instances from running migrations simultaneously.
- If the lock is not acquired, it retries every 5 seconds for up to 60 seconds.
-
Checking and Executing Migrations:
- DbUp checks if any new migration scripts exist.
- If migration is required, the scripts are executed sequentially using
DeployChanges.To.PostgresqlDatabase(dbConnectionString).WithScriptsFromFileSystem(migrationPath).Build();. - If migration succeeds, seed scripts are executed.
- Note: All migration, rollback, and seed data scripts should be idempotent to avoid issues when executed multiple times.
-
Handling Multiple Iterations:
- Each time the application starts, DbUp checks for new migration scripts.
- Already applied scripts are skipped since DbUp tracks execution history in the database.
- New scripts (added in subsequent deployments) are applied in order.
- This ensures an incremental migration approach without reapplying old scripts.
-
Releasing the Advisory Lock:
- Once migrations and seed scripts complete, the advisory lock is released.
This process guarantees that only new scripts are executed while maintaining consistency and preventing race conditions in multi-instance environments.
- Rollbacks are manual with DbUp (DbUp does not support automatic rollbacks).
- Always test rollback scripts before deployment.
- Ensure rollback scripts reverse the exact changes made in migration scripts.
-
Simplicity
- No complex ORM setup is required.
- Migration scripts are plain SQL, making them easy to write and review.
-
Transactional Migrations
- Ensures that database schema changes are atomic and consistent.
-
Version Control Friendly
- Migration scripts are stored as files, allowing version control.
-
Environment-Specific Seeding
- Enables different seed data for different environments (Dev, QA, UAT).
-
Concurrency Handling
- Advisory locks prevent multiple instances from running migrations simultaneously.
-
Logging Support
- Logs migration progress using
DbUpLoggerExtension.
- Logs migration progress using
-
No Built-in Rollback Support
- DbUp does not provide automatic rollback functionality. Rollbacks must be managed manually via separate scripts in the
Rollbacksfolder.
- DbUp does not provide automatic rollback functionality. Rollbacks must be managed manually via separate scripts in the
-
Sequential Execution Requirement
- Scripts execute in the order they are found in the directory; changing script execution order requires renaming files.
-
Limited ORM Integration
- DbUp is not an ORM and does not integrate directly with Entity Framework.
-
No Schema Diffing
- Unlike tools such as Liquibase or Flyway, DbUp does not generate schema diffs automatically.
DbUp provides a lightweight, script-based approach to database migrations in .NET API projects. While it lacks built-in rollback and schema diffing features, it is simple, easy to integrate, and supports transactional execution, making it a reliable choice for managing database schema changes in database.