From 243931eb6aa48a7b46df0a74fd9ff7b630b02ae5 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sun, 8 Feb 2026 14:57:45 -0500 Subject: [PATCH] Refactor database connection handling and update environment variable usage across Docker configurations --- .env.example | 18 +++++++ .gitignore | 5 +- docker-compose.db.yaml | 25 ++++++---- docker-compose.dev.yaml | 24 ++++++--- docker-compose.prod.yaml | 19 ++++--- docker-compose.test.yaml | 27 +++++++--- src/Core/API/API.Specs/Dockerfile | 2 +- src/Core/API/API.Specs/TestApiFactory.cs | 5 -- .../Database/Database.Migrations/Program.cs | 35 ++++++++++++- src/Core/Database/Database.Seed/Program.cs | 32 +++++++++++- .../Sql/DefaultSqlConnectionFactory.cs | 35 +++++++++++-- .../Sql/SqlConnectionStringHelper.cs | 50 +++++++++++++++++++ .../Repository/Repository.Tests/Dockerfile | 2 +- 13 files changed, 234 insertions(+), 45 deletions(-) create mode 100644 .env.example create mode 100644 src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b0b20fb --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# ====================== +# Database Configuration +# ====================== + +# SQL Server Connection Components +# These are used to build connection strings dynamically +DB_SERVER=sqlserver,1433 +DB_NAME=Biergarten +DB_USER=sa +DB_PASSWORD=YourStrong!Passw0rd + +# ====================== +# JWT Configuration +# ====================== + +# JWT Secret for signing tokens (generate using: openssl rand -base64 32) +JWT_SECRET=128490218jfklsdajfdsa90f8sd0fid0safasr31jl2k1j4AFSDR + diff --git a/.gitignore b/.gitignore index b06b40a..ecede2b 100644 --- a/.gitignore +++ b/.gitignore @@ -486,4 +486,7 @@ FodyWeavers.xsd database -.env.* \ No newline at end of file +.env +.env.dev +.env.test +.env.prod diff --git a/docker-compose.db.yaml b/docker-compose.db.yaml index 5db74df..4c359b3 100644 --- a/docker-compose.db.yaml +++ b/docker-compose.db.yaml @@ -1,25 +1,27 @@ services: - sqlserver: + sqlserver: + env_file: ".env.dev" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: dev-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_PASSWORD}" MSSQL_PID: "Express" ports: - "1433:1433" volumes: - sqlserverdata-dev:/var/opt/mssql healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 start_period: 30s networks: - devnet - database.migrations: + database.migrations: + env_file: ".env.dev" image: database.migrations container_name: dev-env-database-migrations depends_on: @@ -33,14 +35,17 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" CLEAR_DATABASE: "true" restart: "no" networks: - devnet - database.seed: +database.seed: + env_file: ".env.dev" image: database.seed container_name: dev-env-database-seed depends_on: @@ -54,11 +59,13 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - devnet - volumes: sqlserverdata-dev: driver: local diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 26eedfc..f764ee0 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,18 +1,19 @@ services: sqlserver: + env_file: ".env.dev" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: dev-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_PASSWORD}" MSSQL_PID: "Express" ports: - "1433:1433" volumes: - sqlserverdata-dev:/var/opt/mssql healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -20,6 +21,7 @@ services: networks: - devnet database.migrations: + env_file: ".env.dev" image: database.migrations container_name: dev-env-database-migrations depends_on: @@ -33,14 +35,17 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" CLEAR_DATABASE: "true" restart: "no" networks: - devnet database.seed: + env_file: ".env.dev" image: database.seed container_name: dev-env-database-seed depends_on: @@ -54,12 +59,16 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - devnet api.core: + env_file: ".env.dev" image: api.core container_name: dev-env-api-core depends_on: @@ -78,7 +87,10 @@ services: ASPNETCORE_ENVIRONMENT: "Development" ASPNETCORE_URLS: "http://0.0.0.0:8080" DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" JWT_SECRET: "${JWT_SECRET}" restart: unless-stopped networks: diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index eccbf98..d41d268 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -1,16 +1,17 @@ services: sqlserver: + env_file: ".env.prod" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: prod-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_PASSWORD}" MSSQL_PID: "Express" volumes: - sqlserverdata-prod:/var/opt/mssql healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -19,6 +20,7 @@ services: - prodnet database.migrations: + env_file: ".env.prod" image: database.migrations container_name: prod-env-database-migrations depends_on: @@ -32,13 +34,16 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - prodnet api.core: + env_file: ".env.prod" image: api.core container_name: prod-env-api-core depends_on: @@ -57,8 +62,10 @@ services: ASPNETCORE_ENVIRONMENT: "Production" ASPNETCORE_URLS: "http://0.0.0.0:8080" DOTNET_RUNNING_IN_CONTAINER: "true" - MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" - DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" JWT_SECRET: "${JWT_SECRET}" restart: unless-stopped networks: diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 08e93b7..17b2cf2 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -1,17 +1,18 @@ services: sqlserver: + env_file: ".env.test" image: mcr.microsoft.com/mssql/server:2022-latest platform: linux/amd64 container_name: test-env-sqlserver environment: ACCEPT_EULA: "Y" - SA_PASSWORD: "${SA_PASSWORD}" + SA_PASSWORD: "${DB_PASSWORD}" MSSQL_PID: "Express" DOTNET_RUNNING_IN_CONTAINER: "true" volumes: - sqlserverdata-test:/var/opt/mssql healthcheck: - test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 12 @@ -20,6 +21,7 @@ services: - testnet database.migrations: + env_file: ".env.test" image: database.migrations container_name: test-env-database-migrations depends_on: @@ -33,14 +35,17 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" - MASTER_DB_CONNECTION_STRING: "${TEST_MASTER_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" CLEAR_DATABASE: "true" restart: "no" networks: - testnet database.seed: + env_file: ".env.test" image: database.seed container_name: test-env-database-seed depends_on: @@ -54,12 +59,16 @@ services: APP_UID: 1000 environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" restart: "no" networks: - testnet api.specs: + env_file: ".env.test" image: api.specs container_name: test-env-api-specs depends_on: @@ -72,7 +81,10 @@ services: BUILD_CONFIGURATION: Release environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + DB_SERVER: "${DB_SERVER}" + DB_NAME: "${DB_NAME}" + DB_USER: "${DB_USER}" + DB_PASSWORD: "${DB_PASSWORD}" JWT_SECRET: "${JWT_SECRET}" volumes: - ./test-results:/app/test-results @@ -81,6 +93,7 @@ services: - testnet repository.tests: + env_file: ".env.test" image: repository.tests container_name: test-env-repository-tests depends_on: @@ -93,8 +106,6 @@ services: BUILD_CONFIGURATION: Release environment: DOTNET_RUNNING_IN_CONTAINER: "true" - DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" - JWT_SECRET: "${JWT_SECRET}" volumes: - ./test-results:/app/test-results restart: "no" diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile index 7929db8..97139c9 100644 --- a/src/Core/API/API.Specs/Dockerfile +++ b/src/Core/API/API.Specs/Dockerfile @@ -16,4 +16,4 @@ WORKDIR /src RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* RUN mkdir -p /app/test-results WORKDIR /src/API/API.Specs -ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--no-build", "--no-restore", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"] +ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"] diff --git a/src/Core/API/API.Specs/TestApiFactory.cs b/src/Core/API/API.Specs/TestApiFactory.cs index 36e7820..15da2c8 100644 --- a/src/Core/API/API.Specs/TestApiFactory.cs +++ b/src/Core/API/API.Specs/TestApiFactory.cs @@ -10,11 +10,6 @@ namespace API.Specs protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); - - builder.ConfigureAppConfiguration((context, configBuilder) => - { - var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); - }); } } } diff --git a/src/Core/Database/Database.Migrations/Program.cs b/src/Core/Database/Database.Migrations/Program.cs index 86281cb..af80640 100644 --- a/src/Core/Database/Database.Migrations/Program.cs +++ b/src/Core/Database/Database.Migrations/Program.cs @@ -7,8 +7,39 @@ namespace Database.Migrations; public static class Program { - private static readonly string? connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); - private static readonly string? masterConnectionString = Environment.GetEnvironmentVariable("MASTER_DB_CONNECTION_STRING"); + private static string BuildConnectionString(string? databaseName = null) + { + var server = Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + + var dbName = databaseName + ?? Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + + var user = Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + + var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") + ?? "True"; + + var builder = new SqlConnectionStringBuilder + { + DataSource = server, + InitialCatalog = dbName, + UserID = user, + Password = password, + TrustServerCertificate = bool.Parse(trustServerCertificate), + Encrypt = true + }; + + return builder.ConnectionString; + } + + private static readonly string connectionString = BuildConnectionString(); + private static readonly string masterConnectionString = BuildConnectionString("master"); private static bool DeployMigrations() { diff --git a/src/Core/Database/Database.Seed/Program.cs b/src/Core/Database/Database.Seed/Program.cs index 0fb1138..e7fff96 100644 --- a/src/Core/Database/Database.Seed/Program.cs +++ b/src/Core/Database/Database.Seed/Program.cs @@ -3,9 +3,39 @@ using Microsoft.Data.SqlClient; using DbUp; using System.Reflection; +string BuildConnectionString() +{ + var server = Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + + var dbName = Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + + var user = Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + + var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") + ?? "True"; + + var builder = new SqlConnectionStringBuilder + { + DataSource = server, + InitialCatalog = dbName, + UserID = user, + Password = password, + TrustServerCertificate = bool.Parse(trustServerCertificate), + Encrypt = true + }; + + return builder.ConnectionString; +} + try { - var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); + var connectionString = BuildConnectionString(); Console.WriteLine("Attempting to connect to database..."); diff --git a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs index eb8abad..b38f25d 100644 --- a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs +++ b/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs @@ -7,11 +7,36 @@ namespace DataAccessLayer.Sql { public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory { - private readonly string _connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") - ?? configuration.GetConnectionString("Default") - ?? throw new InvalidOperationException( - "Database connection string not configured. Set DB_CONNECTION_STRING env var or ConnectionStrings:Default." - ); + private readonly string _connectionString = GetConnectionString(configuration); + + private static string GetConnectionString(IConfiguration configuration) + { + // Check for full connection string first + var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); + if (!string.IsNullOrEmpty(fullConnectionString)) + { + return fullConnectionString; + } + + // Try to build from individual environment variables (preferred method for Docker) + try + { + return SqlConnectionStringHelper.BuildConnectionString(); + } + catch (InvalidOperationException) + { + // Fall back to configuration-based connection string if env vars are not set + var connString = configuration.GetConnectionString("Default"); + if (!string.IsNullOrEmpty(connString)) + { + return connString; + } + + throw new InvalidOperationException( + "Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default." + ); + } + } public DbConnection CreateConnection() { diff --git a/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs b/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs new file mode 100644 index 0000000..907c82f --- /dev/null +++ b/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs @@ -0,0 +1,50 @@ +using Microsoft.Data.SqlClient; + +namespace DataAccessLayer.Sql +{ + public static class SqlConnectionStringHelper + { + /// + /// Builds a SQL Server connection string from environment variables. + /// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE. + /// + /// Optional override for the database name. If null, uses DB_NAME env var. + /// A properly formatted SQL Server connection string. + public static string BuildConnectionString(string? databaseName = null) + { + var server = Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + + var dbName = databaseName + ?? Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + + var user = Environment.GetEnvironmentVariable("DB_USER") + ?? throw new InvalidOperationException("DB_USER environment variable is not set"); + + var password = Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); + + var builder = new SqlConnectionStringBuilder + { + DataSource = server, + InitialCatalog = dbName, + UserID = user, + Password = password, + TrustServerCertificate = true, + Encrypt = true + }; + + return builder.ConnectionString; + } + + /// + /// Builds a connection string to the master database using environment variables. + /// + /// A connection string for the master database. + public static string BuildMasterConnectionString() + { + return BuildConnectionString("master"); + } + } +} diff --git a/src/Core/Repository/Repository.Tests/Dockerfile b/src/Core/Repository/Repository.Tests/Dockerfile index 6d5db48..4b83399 100644 --- a/src/Core/Repository/Repository.Tests/Dockerfile +++ b/src/Core/Repository/Repository.Tests/Dockerfile @@ -11,4 +11,4 @@ RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/bui FROM build AS final RUN mkdir -p /app/test-results WORKDIR /src/Repository/Repository.Tests -ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--no-build", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"] +ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]