diff --git a/docker-compose.db.yaml b/docker-compose.db.yaml
new file mode 100644
index 0000000..5db74df
--- /dev/null
+++ b/docker-compose.db.yaml
@@ -0,0 +1,70 @@
+services:
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server:2022-latest
+ platform: linux/amd64
+ container_name: dev-env-sqlserver
+ environment:
+ ACCEPT_EULA: "Y"
+ SA_PASSWORD: "${SA_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"]
+ interval: 10s
+ timeout: 5s
+ retries: 12
+ start_period: 30s
+ networks:
+ - devnet
+ database.migrations:
+ image: database.migrations
+ container_name: dev-env-database-migrations
+ depends_on:
+ sqlserver:
+ condition: service_healthy
+ build:
+ context: ./src/Core/Database
+ dockerfile: Database.Migrations/Dockerfile
+ args:
+ BUILD_CONFIGURATION: Release
+ APP_UID: 1000
+ environment:
+ DOTNET_RUNNING_IN_CONTAINER: "true"
+ DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
+ MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}"
+ CLEAR_DATABASE: "true"
+ restart: "no"
+ networks:
+ - devnet
+
+ database.seed:
+ image: database.seed
+ container_name: dev-env-database-seed
+ depends_on:
+ database.migrations:
+ condition: service_completed_successfully
+ build:
+ context: ./src/Core
+ dockerfile: Database/Database.Seed/Dockerfile
+ args:
+ BUILD_CONFIGURATION: Release
+ APP_UID: 1000
+ environment:
+ DOTNET_RUNNING_IN_CONTAINER: "true"
+ DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
+ restart: "no"
+ networks:
+ - devnet
+
+volumes:
+ sqlserverdata-dev:
+ driver: local
+ nuget-cache-dev:
+ driver: local
+
+networks:
+ devnet:
+ driver: bridge
diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml
index 6fff877..26eedfc 100644
--- a/docker-compose.dev.yaml
+++ b/docker-compose.dev.yaml
@@ -35,15 +35,36 @@ services:
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}"
+ CLEAR_DATABASE: "true"
restart: "no"
networks:
- devnet
+
+ database.seed:
+ image: database.seed
+ container_name: dev-env-database-seed
+ depends_on:
+ database.migrations:
+ condition: service_completed_successfully
+ build:
+ context: ./src/Core
+ dockerfile: Database/Database.Seed/Dockerfile
+ args:
+ BUILD_CONFIGURATION: Release
+ APP_UID: 1000
+ environment:
+ DOTNET_RUNNING_IN_CONTAINER: "true"
+ DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
+ restart: "no"
+ networks:
+ - devnet
+
api.core:
image: api.core
container_name: dev-env-api-core
depends_on:
- sqlserver:
- condition: service_healthy
+ database.seed:
+ condition: service_completed_successfully
build:
context: ./src/Core
dockerfile: API/API.Core/Dockerfile
@@ -58,6 +79,7 @@ services:
ASPNETCORE_URLS: "http://0.0.0.0:8080"
DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
+ JWT_SECRET: "${JWT_SECRET}"
restart: unless-stopped
networks:
- devnet
diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml
index 7efaaba..eccbf98 100644
--- a/docker-compose.prod.yaml
+++ b/docker-compose.prod.yaml
@@ -59,6 +59,7 @@ services:
DOTNET_RUNNING_IN_CONTAINER: "true"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
+ JWT_SECRET: "${JWT_SECRET}"
restart: unless-stopped
networks:
- prodnet
diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml
new file mode 100644
index 0000000..08e93b7
--- /dev/null
+++ b/docker-compose.test.yaml
@@ -0,0 +1,110 @@
+services:
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server:2022-latest
+ platform: linux/amd64
+ container_name: test-env-sqlserver
+ environment:
+ ACCEPT_EULA: "Y"
+ SA_PASSWORD: "${SA_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"]
+ interval: 10s
+ timeout: 5s
+ retries: 12
+ start_period: 30s
+ networks:
+ - testnet
+
+ database.migrations:
+ image: database.migrations
+ container_name: test-env-database-migrations
+ depends_on:
+ sqlserver:
+ condition: service_healthy
+ build:
+ context: ./src/Core/Database
+ dockerfile: Database.Migrations/Dockerfile
+ args:
+ BUILD_CONFIGURATION: Release
+ 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}"
+ CLEAR_DATABASE: "true"
+ restart: "no"
+ networks:
+ - testnet
+
+ database.seed:
+ image: database.seed
+ container_name: test-env-database-seed
+ depends_on:
+ database.migrations:
+ condition: service_completed_successfully
+ build:
+ context: ./src/Core
+ dockerfile: Database/Database.Seed/Dockerfile
+ args:
+ BUILD_CONFIGURATION: Release
+ APP_UID: 1000
+ environment:
+ DOTNET_RUNNING_IN_CONTAINER: "true"
+ DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}"
+ restart: "no"
+ networks:
+ - testnet
+
+ api.specs:
+ image: api.specs
+ container_name: test-env-api-specs
+ depends_on:
+ database.seed:
+ condition: service_completed_successfully
+ build:
+ context: ./src/Core
+ dockerfile: API/API.Specs/Dockerfile
+ args:
+ 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"
+ networks:
+ - testnet
+
+ repository.tests:
+ image: repository.tests
+ container_name: test-env-repository-tests
+ depends_on:
+ database.seed:
+ condition: service_completed_successfully
+ build:
+ context: ./src/Core
+ dockerfile: Repository/Repository.Tests/Dockerfile
+ args:
+ 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"
+ networks:
+ - testnet
+
+volumes:
+ sqlserverdata-test:
+ driver: local
+
+networks:
+ testnet:
+ driver: bridge
diff --git a/src/Core/API/API.Specs/API.Specs.csproj b/src/Core/API/API.Specs/API.Specs.csproj
index 0791bcb..45ce21a 100644
--- a/src/Core/API/API.Specs/API.Specs.csproj
+++ b/src/Core/API/API.Specs/API.Specs.csproj
@@ -34,6 +34,5 @@
-
diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile
new file mode 100644
index 0000000..7929db8
--- /dev/null
+++ b/src/Core/API/API.Specs/Dockerfile
@@ -0,0 +1,19 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
+COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
+COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
+COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"]
+RUN dotnet restore "API/API.Specs/API.Specs.csproj"
+COPY . .
+WORKDIR "/src/API/API.Specs"
+RUN dotnet build "./API.Specs.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS final
+ARG BUILD_CONFIGURATION=Release
+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"]
diff --git a/src/Core/Database/Database.Migrations/Database.Migrations.csproj b/src/Core/Database/Database.Migrations/Database.Migrations.csproj
index 3488889..ab0b5d7 100644
--- a/src/Core/Database/Database.Migrations/Database.Migrations.csproj
+++ b/src/Core/Database/Database.Migrations/Database.Migrations.csproj
@@ -4,7 +4,7 @@
net10.0
enable
enable
- DataLayer
+ Database.Migrations
Linux
diff --git a/src/Core/Database/Database.Migrations/Program.cs b/src/Core/Database/Database.Migrations/Program.cs
index 1fa4b3a..86281cb 100644
--- a/src/Core/Database/Database.Migrations/Program.cs
+++ b/src/Core/Database/Database.Migrations/Program.cs
@@ -3,7 +3,7 @@ using System.Reflection;
using DbUp;
using Microsoft.Data.SqlClient;
-namespace DataLayer;
+namespace Database.Migrations;
public static class Program
{
@@ -22,6 +22,57 @@ public static class Program
return result.Successful;
}
+ private static bool ClearDatabase()
+ {
+ var myConn = new SqlConnection(masterConnectionString);
+
+ try
+ {
+ myConn.Open();
+
+ // First, set the database to single user mode to close all connections
+ var setModeCommand = new SqlCommand(
+ "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 (System.Exception ex)
+ {
+ Console.WriteLine($"Warning: Could not set single user mode: {ex.Message}");
+ }
+
+ // Then drop the database
+ var dropCommand = new SqlCommand("DROP DATABASE IF EXISTS [Biergarten];", myConn);
+ try
+ {
+ dropCommand.ExecuteNonQuery();
+ Console.WriteLine("Database cleared successfully.");
+ }
+ catch (System.Exception ex)
+ {
+ Console.WriteLine($"Error dropping database: {ex}");
+ return false;
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Console.WriteLine($"Error clearing database: {ex}");
+ return false;
+ }
+ finally
+ {
+ if (myConn.State == ConnectionState.Open)
+ {
+ myConn.Close();
+ }
+ }
+ return true;
+ }
+
private static bool CreateDatabaseIfNotExists()
{
var myConn = new SqlConnection(masterConnectionString);
@@ -58,6 +109,13 @@ public static class Program
try
{
+ var clearDatabase = Environment.GetEnvironmentVariable("CLEAR_DATABASE");
+ if (clearDatabase == "true")
+ {
+ Console.WriteLine("CLEAR_DATABASE is enabled. Clearing existing database...");
+ ClearDatabase();
+ }
+
CreateDatabaseIfNotExists();
var success = DeployMigrations();
diff --git a/src/Core/Database/Database.Seed/Database.Seed.csproj b/src/Core/Database/Database.Seed/Database.Seed.csproj
index 656d3be..4c68909 100644
--- a/src/Core/Database/Database.Seed/Database.Seed.csproj
+++ b/src/Core/Database/Database.Seed/Database.Seed.csproj
@@ -4,7 +4,7 @@
net10.0
enable
enable
- DBSeed
+ Database.Seed
@@ -18,7 +18,6 @@
-
diff --git a/src/Core/Database/Database.Seed/Dockerfile b/src/Core/Database/Database.Seed/Dockerfile
new file mode 100644
index 0000000..1bfaf89
--- /dev/null
+++ b/src/Core/Database/Database.Seed/Dockerfile
@@ -0,0 +1,16 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+# Copy everything from the context (src/Core)
+COPY . .
+RUN dotnet restore "./Database/Database.Seed/Database.Seed.csproj"
+RUN dotnet build "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Database.Seed.dll"]
diff --git a/src/Core/Database/Database.Seed/Program.cs b/src/Core/Database/Database.Seed/Program.cs
index 720005b..0fb1138 100644
--- a/src/Core/Database/Database.Seed/Program.cs
+++ b/src/Core/Database/Database.Seed/Program.cs
@@ -5,73 +5,60 @@ using System.Reflection;
try
{
- var connectionString = Environment.GetEnvironmentVariable(
- "DB_CONNECTION_STRING"
- );
- if (string.IsNullOrWhiteSpace(connectionString))
- throw new InvalidOperationException(
- "Environment variable DB_CONNECTION_STRING is not set or is empty."
- );
+ var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
- await using var connection = new SqlConnection(connectionString);
- await connection.OpenAsync();
+ Console.WriteLine("Attempting to connect to database...");
- // drop and recreate the database
- var useMaster = connection.CreateCommand();
- useMaster.CommandText = "USE master;";
- await useMaster.ExecuteNonQueryAsync();
+ // Retry logic for database connection
+ SqlConnection? connection = null;
+ int maxRetries = 10;
+ int retryDelayMs = 2000;
- var dbName = "Biergarten";
- var dropDb = connection.CreateCommand();
- dropDb.CommandText = $@"
- IF DB_ID(N'{dbName}') IS NOT NULL
- BEGIN
- ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
- DROP DATABASE [{dbName}];
- END";
- await dropDb.ExecuteNonQueryAsync();
-
- var createDb = connection.CreateCommand();
- createDb.CommandText = $@"CREATE DATABASE [{dbName}];";
- await createDb.ExecuteNonQueryAsync();
- await connection.CloseAsync();
- await connection.OpenAsync();
-
-
- Console.WriteLine("Connected to database.");
-
- Console.WriteLine("Starting migrations...");
-
- // Run Database.Core migrations (embedded resources) via DbUp
- var migrationAssembly = Assembly.Load("Database.Core");
- var upgrader = DeployChanges
- .To.SqlDatabase(connectionString)
- .WithScriptsEmbeddedInAssembly(migrationAssembly)
- .LogToConsole()
- .Build();
-
- var upgradeResult = upgrader.PerformUpgrade();
- if (!upgradeResult.Successful)
- throw upgradeResult.Error;
-
- Console.WriteLine("Migrations completed.");
-
-
- ISeeder[] seeders =
- [
- new LocationSeeder(),
- new UserSeeder(),
- ];
-
- foreach (var seeder in seeders)
+ for (int attempt = 1; attempt <= maxRetries; attempt++)
{
- Console.WriteLine($"Seeding {seeder.GetType().Name}...");
- await seeder.SeedAsync(connection);
- Console.WriteLine($"{seeder.GetType().Name} seeded.");
+ try
+ {
+ connection = new SqlConnection(connectionString);
+ await connection.OpenAsync();
+ Console.WriteLine($"Connected to database successfully on attempt {attempt}.");
+ break;
+ }
+ catch (SqlException ex) when (attempt < maxRetries)
+ {
+ Console.WriteLine($"Connection attempt {attempt}/{maxRetries} failed: {ex.Message}");
+ Console.WriteLine($"Retrying in {retryDelayMs}ms...");
+ await Task.Delay(retryDelayMs);
+ connection?.Dispose();
+ connection = null;
+ }
+ }
+
+ if (connection == null)
+ {
+ throw new Exception($"Failed to connect to database after {maxRetries} attempts.");
+ }
+
+ Console.WriteLine("Starting seeding...");
+
+ using (connection)
+ {
+
+ ISeeder[] seeders =
+ [
+ new LocationSeeder(),
+ new UserSeeder(),
+ ];
+
+ foreach (var seeder in seeders)
+ {
+ Console.WriteLine($"Seeding {seeder.GetType().Name}...");
+ await seeder.SeedAsync(connection);
+ Console.WriteLine($"{seeder.GetType().Name} seeded.");
+ }
+
+ Console.WriteLine("Seed completed successfully.");
}
- Console.WriteLine("Seed completed successfully.");
- await connection.CloseAsync();
return 0;
}
catch (Exception ex)
diff --git a/src/Core/Repository/Repository.Tests/Dockerfile b/src/Core/Repository/Repository.Tests/Dockerfile
new file mode 100644
index 0000000..6d5db48
--- /dev/null
+++ b/src/Core/Repository/Repository.Tests/Dockerfile
@@ -0,0 +1,14 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"]
+COPY ["Repository/Repository.Tests/Repository.Tests.csproj", "Repository/Repository.Tests/"]
+RUN dotnet restore "Repository/Repository.Tests/Repository.Tests.csproj"
+COPY . .
+WORKDIR "/src/Repository/Repository.Tests"
+RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+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"]
diff --git a/src/Core/Service/Service.Core/Services/JwtService.cs b/src/Core/Service/Service.Core/Services/JwtService.cs
index c881e70..197dd21 100644
--- a/src/Core/Service/Service.Core/Services/JwtService.cs
+++ b/src/Core/Service/Service.Core/Services/JwtService.cs
@@ -1,3 +1,4 @@
+using System;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
@@ -6,14 +7,13 @@ using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace ServiceCore.Services;
-public class JwtService(IConfiguration config) : IJwtService
+public class JwtService : IJwtService
{
- // private readonly string? _secret = config["Jwt:Secret"];
- private readonly string? _secret = "128490218jfklsdajfdsa90f8sd0fid0safasr31jl2k1j4AFSDR!@#$fdsafjdslajfl";
+ private readonly string? _secret = Environment.GetEnvironmentVariable("JWT_SECRET");
public string GenerateJwt(Guid userId, string username, DateTime expiry)
{
var handler = new JsonWebTokenHandler();
-
+
var key = Encoding.UTF8.GetBytes(_secret ?? throw new InvalidOperationException("secret not set"));
// Base claims (always present)
@@ -35,4 +35,4 @@ public class JwtService(IConfiguration config) : IJwtService
return handler.CreateToken(tokenDescriptor);
}
-}
\ No newline at end of file
+}