diff --git a/web/backend/Database/Database.Migrations/Program.cs b/web/backend/Database/Database.Migrations/Program.cs
index 672648c8..5ce63a50 100644
--- a/web/backend/Database/Database.Migrations/Program.cs
+++ b/web/backend/Database/Database.Migrations/Program.cs
@@ -7,215 +7,211 @@ 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.
+/// Entry point for the database migration runner. Reads connection details from environment
+/// variables, optionally drops 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();
+ ///
+ /// Migration runner entry point. Drops the target database when CLEAR_DATABASE is
+ /// set to true, ensures the database exists, then deploys all pending migrations.
+ ///
+ ///
+ /// 0 if migrations completed successfully; 1 if they failed or an error occurred.
+ ///
+ public static async Task Main()
+ {
+ Console.WriteLine("Starting database migrations...");
- /// The connection string for the master database, used for create/drop operations.
- private static readonly string masterConnectionString = BuildConnectionString("master");
+ try
+ {
+ DatabaseSettings settings = DatabaseSettings.FromEnvironment();
+
+ if (settings.ClearDatabase)
+ {
+ Console.WriteLine(
+ $"CLEAR_DATABASE is enabled. Dropping database '{settings.DatabaseName}'..."
+ );
+ await DropDatabaseIfExistsAsync(settings);
+ }
+
+ await CreateDatabaseIfNotExistsAsync(settings);
+
+ return DeployMigrations(settings.ConnectionString) ? 0 : 1;
+ }
+ catch (Exception ex) when (ex is InvalidOperationException or SqlException)
+ {
+ Console.Error.WriteLine("An error occurred during database migrations:");
+ Console.Error.WriteLine(ex.Message);
+ return 1;
+ }
+ }
///
- /// Builds a SQL Server connection string from the DB_SERVER, DB_NAME,
- /// DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE
- /// environment variables.
+ /// Drops the target database if it exists, first forcing it into single-user mode with
+ /// rollback immediate so that any existing connections are terminated.
///
- ///
- /// 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)
+ /// The resolved database settings.
+ /// Thrown if the connection or the drop operation fails.
+ private static async Task DropDatabaseIfExistsAsync(DatabaseSettings settings)
{
- string server =
- Environment.GetEnvironmentVariable("DB_SERVER")
- ?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
+ const string sql = """
+ DECLARE @statement nvarchar(max);
- string dbName =
- databaseName
- ?? Environment.GetEnvironmentVariable("DB_NAME")
- ?? throw new InvalidOperationException("DB_NAME environment variable is not set");
+ IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
+ BEGIN
+ SET @statement =
+ N'ALTER DATABASE ' + QUOTENAME(@databaseName)
+ + N' SET SINGLE_USER WITH ROLLBACK IMMEDIATE;';
+ EXEC sys.sp_executesql @statement;
- string user =
- Environment.GetEnvironmentVariable("DB_USER")
- ?? throw new InvalidOperationException("DB_USER environment variable is not set");
+ SET @statement = N'DROP DATABASE ' + QUOTENAME(@databaseName) + N';';
+ EXEC sys.sp_executesql @statement;
+ END
+ """;
- string password =
- Environment.GetEnvironmentVariable("DB_PASSWORD")
- ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
+ await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
+ Console.WriteLine($"Database '{settings.DatabaseName}' dropped, or did not exist.");
+ }
- string trustServerCertificate =
- Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True";
+ ///
+ /// Creates the target database on the master connection if it does not already exist.
+ ///
+ /// The resolved database settings.
+ /// Thrown if the connection or the create operation fails.
+ private static async Task CreateDatabaseIfNotExistsAsync(DatabaseSettings settings)
+ {
+ const string sql = """
+ DECLARE @statement nvarchar(max);
- SqlConnectionStringBuilder builder = new()
- {
- DataSource = server,
- InitialCatalog = dbName,
- UserID = user,
- Password = password,
- TrustServerCertificate = bool.Parse(trustServerCertificate),
- Encrypt = true,
- };
+ IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = @databaseName)
+ BEGIN
+ SET @statement = N'CREATE DATABASE ' + QUOTENAME(@databaseName) + N';';
+ EXEC sys.sp_executesql @statement;
+ END
+ """;
- return builder.ConnectionString;
+ await ExecuteNonQueryAsync(settings.MasterConnectionString, sql, settings.DatabaseName);
+ Console.WriteLine($"Database '{settings.DatabaseName}' is present.");
+ }
+
+ ///
+ /// Opens a connection, executes a non-query batch parameterised by database name, and
+ /// disposes both the command and the connection.
+ ///
+ /// The connection string to open.
+ /// The Transact-SQL batch to execute.
+ /// The value bound to the @databaseName parameter.
+ private static async Task ExecuteNonQueryAsync(
+ string connectionString,
+ string sql,
+ string databaseName
+ )
+ {
+ await using SqlConnection connection = new(connectionString);
+ await connection.OpenAsync();
+
+ await using SqlCommand command = new(sql, connection);
+ command.Parameters.Add("@databaseName", SqlDbType.NVarChar, 128).Value = databaseName;
+
+ await command.ExecuteNonQueryAsync();
}
///
/// Applies all pending SQL migration scripts embedded in this assembly to the target
/// database using DbUp, logging progress to the console.
///
+ /// The connection string for the target application database.
/// true if the upgrade completed successfully; otherwise false.
- private static bool DeployMigrations()
+ private static bool DeployMigrations(string connectionString)
{
- UpgradeEngine? upgrader = DeployChanges
+ UpgradeEngine upgrader = DeployChanges
.To.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
+ .WithTransactionPerScript()
.LogToConsole()
.Build();
- DatabaseUpgradeResult? result = upgrader.PerformUpgrade();
- return result.Successful;
+ DatabaseUpgradeResult result = upgrader.PerformUpgrade();
+
+ if (result.Successful)
+ {
+ Console.WriteLine("Database migrations completed successfully.");
+ return true;
+ }
+
+ Console.Error.WriteLine($"Database migrations failed: {result.Error}");
+ return false;
}
///
- /// Drops the Biergarten database if it exists, first forcing it into single-user
- /// mode with rollback to terminate any existing connections.
+ /// Connection details and runtime options resolved from environment variables.
///
- ///
- /// 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()
+ /// The connection string for the target application database.
+ ///
+ /// The connection string for the master database, used for create and drop operations.
+ ///
+ /// The name of the target application database.
+ /// Whether the target database should be dropped before migrating.
+ private sealed record DatabaseSettings(
+ string ConnectionString,
+ string MasterConnectionString,
+ string DatabaseName,
+ bool ClearDatabase
+ )
{
- SqlConnection myConn = new(masterConnectionString);
-
- try
+ ///
+ /// Builds the settings from the DB_SERVER, DB_NAME, DB_USER,
+ /// DB_PASSWORD, DB_TRUST_SERVER_CERTIFICATE, and CLEAR_DATABASE
+ /// environment variables.
+ ///
+ ///
+ /// Thrown when a required environment variable is not set.
+ ///
+ public static DatabaseSettings FromEnvironment()
{
- myConn.Open();
+ string server = Required("DB_SERVER");
+ string databaseName = Required("DB_NAME");
+ string user = Required("DB_USER");
+ string password = Required("DB_PASSWORD");
- // 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
+ bool trustServerCertificate =
+ !bool.TryParse(
+ Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE"),
+ out bool parsedTrust
+ )
+ || parsedTrust;
+
+ bool clearDatabase = string.Equals(
+ Environment.GetEnvironmentVariable("CLEAR_DATABASE"),
+ "true",
+ StringComparison.OrdinalIgnoreCase
);
- 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 new DatabaseSettings(
+ Build(databaseName),
+ Build("master"),
+ databaseName,
+ clearDatabase
+ );
+
+ string Build(string initialCatalog) =>
+ new SqlConnectionStringBuilder
+ {
+ DataSource = server,
+ InitialCatalog = initialCatalog,
+ UserID = user,
+ Password = password,
+ Encrypt = true,
+ TrustServerCertificate = trustServerCertificate
+ }.ConnectionString;
}
- return true;
+ private static string Required(string variableName) =>
+ Environment.GetEnvironmentVariable(variableName)
+ ?? throw new InvalidOperationException(
+ $"The {variableName} environment variable is not set."
+ );
}
-
- ///
- /// 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;
- }
- }
-}
+}
\ No newline at end of file
diff --git a/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs b/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs
index 32bb3242..fa07f2cc 100644
--- a/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs
+++ b/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs
@@ -1,4 +1,8 @@
-using Microsoft.Data.SqlClient;
+#region
+
+using Microsoft.Data.SqlClient;
+
+#endregion
namespace Database.Seed.DatabaseHelpers;
@@ -9,23 +13,23 @@ public class ConnectionStrings
private static string GetSqlServerConnectionString()
{
- var server =
+ string server =
Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
- var dbName =
+ string dbName =
Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
- var user =
+ string user =
Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException("DB_USER environment variable is not set");
- var password =
+ string password =
Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
- var trustServerCertificate =
+ string trustServerCertificate =
Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True";
SqlConnectionStringBuilder builder = new()
diff --git a/web/backend/Database/Database.Seed/Program.cs b/web/backend/Database/Database.Seed/Program.cs
index 2ead26d6..9442365d 100644
--- a/web/backend/Database/Database.Seed/Program.cs
+++ b/web/backend/Database/Database.Seed/Program.cs
@@ -1,3 +1,5 @@
+
+
using Database.Seed.DatabaseHelpers;
using Database.Seed.PipelineData;
using Database.Seed.Sqlite;
@@ -10,74 +12,67 @@ using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-namespace Database.Seed;
-public class Program
+IReadOnlyList breweries;
+IReadOnlyList users;
+try
{
- public static int Main()
- {
- IReadOnlyList breweries;
- IReadOnlyList users;
- try
- {
- PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
- breweries = reader.ReadBreweryRecords();
- users = reader.ReadUserRecords();
- }
- catch (SqliteException ex)
- {
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine($"Error opening database connection: {ex.Message}");
- Console.ResetColor();
- return 1;
- }
+ PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
+ breweries = reader.ReadBreweryRecords();
+ users = reader.ReadUserRecords();
+}
+catch (SqliteException ex)
+{
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Error opening database connection: {ex.Message}");
+ Console.ResetColor();
+ return 1;
+}
- ServiceCollection services = new();
- services.AddSingleton(new ConfigurationBuilder().Build());
- services.AddSingleton();
- services.AddFeaturesBreweries();
- services.AddFeaturesAuth();
+ServiceCollection services = [];
+services.AddSingleton(new ConfigurationBuilder().Build());
+services.AddSingleton();
+services.AddFeaturesBreweries();
+services.AddFeaturesAuth();
- using ServiceProvider provider = services.BuildServiceProvider();
+using ServiceProvider provider = services.BuildServiceProvider();
- IBreweryRepository breweryRepository;
- IAuthRepository authRepository;
- try
- {
- breweryRepository = provider.GetRequiredService();
- authRepository = provider.GetRequiredService();
- }
- catch (InvalidOperationException ex)
- {
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine($"Error configuring database connection: {ex.Message}");
- Console.ResetColor();
- return 1;
- }
+IBreweryRepository breweryRepository;
+IAuthRepository authRepository;
+try
+{
+ breweryRepository = provider.GetRequiredService();
+ authRepository = provider.GetRequiredService();
+}
+catch (InvalidOperationException ex)
+{
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Error configuring database connection: {ex.Message}");
+ Console.ResetColor();
+ return 1;
+}
- Console.ForegroundColor = ConsoleColor.Green;
- Console.WriteLine(
- $"Loaded {breweries.Count} breweries."
- );
- Console.ResetColor();
+Console.ForegroundColor = ConsoleColor.Green;
+Console.WriteLine(
+ $"Loaded {breweries.Count} breweries."
+);
+Console.ResetColor();
- for (var i = 0; i < breweries.Count; i++)
- {
- BreweryRecord brewery = breweries[i];
- Console.WriteLine(
- $"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
- }
+for (int i = 0; i < breweries.Count; i++)
+{
+ BreweryRecord brewery = breweries[i];
+ Console.WriteLine(
+ $"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
+}
- Console.ForegroundColor = ConsoleColor.Green;
- Console.WriteLine($"Loaded {users.Count} users.");
- Console.ResetColor();
+Console.ForegroundColor = ConsoleColor.Green;
+Console.WriteLine($"Loaded {users.Count} users.");
+Console.ResetColor();
- for (var i = 0; i < users.Count; i++)
- {
- UserRecord user = users[i];
- Console.WriteLine($"{i + 1}:\t{user.User.FirstName} {user.User.LastName}\t ({user.Email})");
- }
+for (int i = 0; i < users.Count; i++)
+{
+ UserRecord user = users[i];
+ Console.WriteLine($"{i + 1}:\t{user.User.FirstName} {user.User.LastName}\t ({user.Email})");
+}
- return 0;
- }
-}
\ No newline at end of file
+return 0;
\ No newline at end of file
diff --git a/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs b/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs
index 16b33d68..9f0ca70f 100644
--- a/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs
+++ b/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs
@@ -1,7 +1,11 @@
+#region
+
using System.Text.Json;
using Database.Seed.PipelineData;
using Microsoft.Data.Sqlite;
+#endregion
+
namespace Database.Seed.Sqlite;
public sealed class PipelineSeedDataReader
diff --git a/web/docker-compose.db.yaml b/web/docker-compose.db.yaml
index be5244ce..2681819a 100644
--- a/web/docker-compose.db.yaml
+++ b/web/docker-compose.db.yaml
@@ -9,7 +9,7 @@ services:
SA_PASSWORD: "${DB_PASSWORD}"
MSSQL_PID: "Express"
ports:
- - "1433:1433"
+ - "1434:1433"
volumes:
- sqlserverdata-dev:/var/opt/mssql
healthcheck:
diff --git a/web/docker-compose.dev.yaml b/web/docker-compose.dev.yaml
index bcf31ac9..1882fd1a 100644
--- a/web/docker-compose.dev.yaml
+++ b/web/docker-compose.dev.yaml
@@ -9,7 +9,7 @@ services:
SA_PASSWORD: "${DB_PASSWORD}"
MSSQL_PID: "Express"
ports:
- - "1433:1433"
+ - "1434:1433"
volumes:
- sqlserverdata-dev:/var/opt/mssql
healthcheck: