using System.Data;
using System.Reflection;
using DbUp;
using DbUp.Engine;
using Microsoft.Data.SqlClient;
namespace Database.Migrations;
///
/// Entry point for the database migration runner. Reads connection details from
/// environment variables, optionally clears and recreates the target database, and
/// applies all SQL migration scripts embedded in this assembly using DbUp.
///
public static class Program
{
/// The connection string for the target application database (DB_NAME).
private static readonly string connectionString = BuildConnectionString();
/// The connection string for the master database, used for create/drop operations.
private static readonly string masterConnectionString = BuildConnectionString("master");
///
/// Builds a SQL Server connection string from the DB_SERVER, DB_NAME,
/// DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE
/// environment variables.
///
///
/// The database (initial catalog) to connect to. When null, falls back to the
/// DB_NAME environment variable.
///
/// A fully built SQL Server connection string.
///
/// Thrown when DB_SERVER, DB_USER, DB_PASSWORD, or (when
/// is null) DB_NAME is not set.
///
private static string BuildConnectionString(string? databaseName = null)
{
string server = Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
string dbName = databaseName
?? Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
string user = Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException("DB_USER environment variable is not set");
string password = Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
string trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE")
?? "True";
SqlConnectionStringBuilder builder = new()
{
DataSource = server,
InitialCatalog = dbName,
UserID = user,
Password = password,
TrustServerCertificate = bool.Parse(trustServerCertificate),
Encrypt = true
};
return builder.ConnectionString;
}
///
/// Applies all pending SQL migration scripts embedded in this assembly to the target
/// database using DbUp, logging progress to the console.
///
/// true if the upgrade completed successfully; otherwise false.
private static bool DeployMigrations()
{
UpgradeEngine? upgrader = DeployChanges
.To.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.LogToConsole()
.Build();
DatabaseUpgradeResult? result = upgrader.PerformUpgrade();
return result.Successful;
}
///
/// Drops the Biergarten database if it exists, first forcing it into single-user
/// mode with rollback to terminate any existing connections.
///
///
/// true if the database was dropped (or did not exist) without error; false
/// if an error occurred while connecting or dropping the database.
///
private static bool ClearDatabase()
{
SqlConnection myConn = new(masterConnectionString);
try
{
myConn.Open();
// First, set the database to single user mode to close all connections
SqlCommand setModeCommand = new(
"IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') " +
"ALTER DATABASE [Biergarten] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;",
myConn);
try
{
setModeCommand.ExecuteNonQuery();
Console.WriteLine("Database set to single user mode.");
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not set single user mode: {ex.Message}");
}
// Then drop the database
SqlCommand dropCommand = new("DROP DATABASE IF EXISTS [Biergarten];", myConn);
try
{
dropCommand.ExecuteNonQuery();
Console.WriteLine("Database cleared successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error dropping database: {ex}");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error clearing database: {ex}");
return false;
}
finally
{
if (myConn.State == ConnectionState.Open) myConn.Close();
}
return true;
}
///
/// Creates the Biergarten database on the master connection if it does not
/// already exist. Errors encountered while creating the database are logged but do
/// not stop execution.
///
/// true always; this method does not propagate database errors as a failure result.
private static bool CreateDatabaseIfNotExists()
{
SqlConnection myConn = new(masterConnectionString);
const string str = """
IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten')
CREATE DATABASE [Biergarten]
""";
SqlCommand myCommand = new(str, myConn);
try
{
myConn.Open();
myCommand.ExecuteNonQuery();
Console.WriteLine("Database creation command executed successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error creating database: {ex}");
}
finally
{
if (myConn.State == ConnectionState.Open) myConn.Close();
}
return true;
}
///
/// Migration runner entry point. Optionally clears the existing database when the
/// CLEAR_DATABASE environment variable is set to "true", ensures the
/// database exists, then deploys all pending migrations.
///
/// Command-line arguments (unused).
/// 0 if migrations completed successfully; 1 if they failed or an error occurred.
public static int Main(string[] args)
{
Console.WriteLine("Starting database migrations...");
try
{
string? clearDatabase = Environment.GetEnvironmentVariable("CLEAR_DATABASE");
if (clearDatabase == "true")
{
Console.WriteLine("CLEAR_DATABASE is enabled. Clearing existing database...");
ClearDatabase();
}
CreateDatabaseIfNotExists();
bool success = DeployMigrations();
if (success)
{
Console.WriteLine("Database migrations completed successfully.");
return 0;
}
Console.WriteLine("Database migrations failed.");
return 1;
}
catch (Exception ex)
{
Console.WriteLine("An error occurred during database migrations:");
Console.WriteLine(ex.Message);
return 1;
}
}
}