From 2411841bdc0f240f94c5abdf42bb02ab575dd6dd Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 00:56:52 -0500 Subject: [PATCH 1/7] create Infrastructure directory --- docker-compose.test.yaml | 2 +- src/Core/API/API.Core/API.Core.csproj | 4 +++- src/Core/API/API.Core/Dockerfile | 5 ++++- src/Core/API/API.Specs/Dockerfile | 5 ++++- src/Core/Core.slnx | 10 +++++++--- .../Database/Database.Seed/Database.Seed.csproj | 3 ++- .../Infrastructure.Jwt}/IJwtService.cs | 0 .../Infrastructure.Jwt/Infrastructure.Jwt.csproj | 13 +++++++++++++ .../Infrastructure.Jwt}/JwtService.cs | 0 .../IPasswordService.cs | 0 .../Infrastructure.PasswordHashing.csproj | 12 ++++++++++++ .../PasswordService.cs | 0 .../Repositories/Auth/AuthRepository.cs | 0 .../Repositories/Auth/IAuthRepository.cs | 0 .../Repository.Core/Repositories/Repository.cs | 0 .../UserAccount/IUserAccountRepository.cs | 0 .../UserAccount/UserAccountRepository.cs | 0 .../Repository.Core/Repository.Core.csproj | 2 +- .../Sql/DefaultSqlConnectionFactory.cs | 0 .../Repository.Core/Sql/ISqlConnectionFactory.cs | 0 .../Sql/SqlConnectionStringHelper.cs | 0 .../Repository.Tests/Auth/AuthRepository.test.cs | 0 .../Repository.Tests/Dockerfile | 15 +++++++++++++++ .../Repository.Tests/Repository.Tests.csproj | 0 .../UserAccount/UserAccountRepository.test.cs | 0 .../Database/TestConnectionFactory.cs | 10 ---------- src/Core/Repository/Repository.Tests/Dockerfile | 14 -------------- src/Core/Service/Service.Core/Service.Core.csproj | 5 ++++- 28 files changed, 66 insertions(+), 34 deletions(-) rename src/Core/{Service/Service.Core/Jwt => Infrastructure/Infrastructure.Jwt}/IJwtService.cs (100%) create mode 100644 src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj rename src/Core/{Service/Service.Core/Jwt => Infrastructure/Infrastructure.Jwt}/JwtService.cs (100%) rename src/Core/{Service/Service.Core/Password => Infrastructure/Infrastructure.PasswordHashing}/IPasswordService.cs (100%) create mode 100644 src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj rename src/Core/{Service/Service.Core/Password => Infrastructure/Infrastructure.PasswordHashing}/PasswordService.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Repositories/Auth/AuthRepository.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Repositories/Auth/IAuthRepository.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Repositories/Repository.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Repository.Core.csproj (91%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Sql/DefaultSqlConnectionFactory.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Sql/ISqlConnectionFactory.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Core/Sql/SqlConnectionStringHelper.cs (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Tests/Auth/AuthRepository.test.cs (100%) create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Tests/Repository.Tests.csproj (100%) rename src/Core/{Repository => Infrastructure/Infrastructure.Repository}/Repository.Tests/UserAccount/UserAccountRepository.test.cs (100%) delete mode 100644 src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs delete mode 100644 src/Core/Repository/Repository.Tests/Dockerfile diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 17b2cf2..4da36ef 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -101,7 +101,7 @@ services: condition: service_completed_successfully build: context: ./src/Core - dockerfile: Repository/Repository.Tests/Dockerfile + dockerfile: Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile args: BUILD_CONFIGURATION: Release environment: diff --git a/src/Core/API/API.Core/API.Core.csproj b/src/Core/API/API.Core/API.Core.csproj index 55b89d1..79d1cc4 100644 --- a/src/Core/API/API.Core/API.Core.csproj +++ b/src/Core/API/API.Core/API.Core.csproj @@ -19,7 +19,9 @@ - + + diff --git a/src/Core/API/API.Core/Dockerfile b/src/Core/API/API.Core/Dockerfile index 7376967..0d7c7e1 100644 --- a/src/Core/API/API.Core/Dockerfile +++ b/src/Core/API/API.Core/Dockerfile @@ -9,7 +9,10 @@ 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 ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"] +COPY ["Domain/Domain.csproj", "Domain/"] +COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"] +COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"] +COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"] COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"] RUN dotnet restore "API/API.Core/API.Core.csproj" COPY . . diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile index 97139c9..2324cf4 100644 --- a/src/Core/API/API.Specs/Dockerfile +++ b/src/Core/API/API.Specs/Dockerfile @@ -3,7 +3,10 @@ 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 ["Domain/Domain.csproj", "Domain/"] +COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"] +COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"] +COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"] COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"] RUN dotnet restore "API/API.Specs/API.Specs.csproj" COPY . . diff --git a/src/Core/Core.slnx b/src/Core/Core.slnx index 8555a8f..00eb33e 100644 --- a/src/Core/Core.slnx +++ b/src/Core/Core.slnx @@ -10,9 +10,13 @@ - - - + + + + + diff --git a/src/Core/Database/Database.Seed/Database.Seed.csproj b/src/Core/Database/Database.Seed/Database.Seed.csproj index 16493b5..0deeae4 100644 --- a/src/Core/Database/Database.Seed/Database.Seed.csproj +++ b/src/Core/Database/Database.Seed/Database.Seed.csproj @@ -19,6 +19,7 @@ - + diff --git a/src/Core/Service/Service.Core/Jwt/IJwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs similarity index 100% rename from src/Core/Service/Service.Core/Jwt/IJwtService.cs rename to src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj new file mode 100644 index 0000000..c7fc3ad --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + Service.Core.Jwt + + + + + + + diff --git a/src/Core/Service/Service.Core/Jwt/JwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs similarity index 100% rename from src/Core/Service/Service.Core/Jwt/JwtService.cs rename to src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs diff --git a/src/Core/Service/Service.Core/Password/IPasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs similarity index 100% rename from src/Core/Service/Service.Core/Password/IPasswordService.cs rename to src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj new file mode 100644 index 0000000..acf2e50 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj @@ -0,0 +1,12 @@ + + + net10.0 + enable + enable + Service.Core.Password + + + + + + diff --git a/src/Core/Service/Service.Core/Password/PasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs similarity index 100% rename from src/Core/Service/Service.Core/Password/PasswordService.cs rename to src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs diff --git a/src/Core/Repository/Repository.Core/Repositories/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Repositories/Auth/AuthRepository.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs diff --git a/src/Core/Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs diff --git a/src/Core/Repository/Repository.Core/Repositories/Repository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Repository.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Repositories/Repository.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Repository.cs diff --git a/src/Core/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs diff --git a/src/Core/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs diff --git a/src/Core/Repository/Repository.Core/Repository.Core.csproj b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj similarity index 91% rename from src/Core/Repository/Repository.Core/Repository.Core.csproj rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj index 6c69c3a..1a3107a 100644 --- a/src/Core/Repository/Repository.Core/Repository.Core.csproj +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj @@ -15,6 +15,6 @@ - + diff --git a/src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs diff --git a/src/Core/Repository/Repository.Core/Sql/ISqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/ISqlConnectionFactory.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Sql/ISqlConnectionFactory.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/ISqlConnectionFactory.cs diff --git a/src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs similarity index 100% rename from src/Core/Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs diff --git a/src/Core/Repository/Repository.Tests/Auth/AuthRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs similarity index 100% rename from src/Core/Repository/Repository.Tests/Auth/AuthRepository.test.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile new file mode 100644 index 0000000..be8f45c --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Domain/Domain.csproj", "Domain/"] +COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"] +COPY ["Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository/Repository.Tests/"] +RUN dotnet restore "Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj" +COPY . . +WORKDIR "/src/Infrastructure/Infrastructure.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/Infrastructure/Infrastructure.Repository/Repository.Tests +ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"] diff --git a/src/Core/Repository/Repository.Tests/Repository.Tests.csproj b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj similarity index 100% rename from src/Core/Repository/Repository.Tests/Repository.Tests.csproj rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj diff --git a/src/Core/Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs similarity index 100% rename from src/Core/Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs rename to src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs diff --git a/src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs b/src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs deleted file mode 100644 index dc15914..0000000 --- a/src/Core/Repository/Repository.Tests/Database/TestConnectionFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Data.Common; -using Repository.Core.Sql; - -namespace Repository.Tests.Database; - -internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory -{ - private readonly DbConnection _conn = conn; - public DbConnection CreateConnection() => _conn; -} diff --git a/src/Core/Repository/Repository.Tests/Dockerfile b/src/Core/Repository/Repository.Tests/Dockerfile deleted file mode 100644 index 4b83399..0000000 --- a/src/Core/Repository/Repository.Tests/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -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", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"] diff --git a/src/Core/Service/Service.Core/Service.Core.csproj b/src/Core/Service/Service.Core/Service.Core.csproj index 9ce9012..595fd7d 100644 --- a/src/Core/Service/Service.Core/Service.Core.csproj +++ b/src/Core/Service/Service.Core/Service.Core.csproj @@ -12,6 +12,9 @@ - + + From f48b8452d3d6352563ed8b23695eaabd4ad10a67 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 01:08:43 -0500 Subject: [PATCH 2/7] Update tests --- .../ValidationExceptionHandlingMiddleware.cs | 47 +++++++++++++++++++ src/Core/API/API.Core/Program.cs | 29 +++++++++++- .../API.Specs/Features/Registration.feature | 1 - 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs diff --git a/src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs b/src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..a631cd0 --- /dev/null +++ b/src/Core/API/API.Core/Middleware/ValidationExceptionHandlingMiddleware.cs @@ -0,0 +1,47 @@ +using System.Net; +using System.Text.Json; +using API.Core.Contracts.Common; +using FluentValidation; + +namespace API.Core.Middleware; + +public class ValidationExceptionHandlingMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (ValidationException ex) + { + await HandleValidationExceptionAsync(context, ex); + } + } + + private static Task HandleValidationExceptionAsync(HttpContext context, ValidationException exception) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + + var errors = exception.Errors + .Select(e => e.ErrorMessage) + .ToList(); + + var message = errors.Count == 1 + ? errors[0] + : "Validation failed. " + string.Join(" ", errors); + + var response = new ResponseBody + { + Message = message + }; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + return context.Response.WriteAsync(JsonSerializer.Serialize(response, jsonOptions)); + } +} diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index 93b762f..b4def9f 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -1,4 +1,6 @@ using FluentValidation; +using FluentValidation.AspNetCore; +using Microsoft.AspNetCore.Mvc; using Repository.Core.Repositories.Auth; using Repository.Core.Repositories.UserAccount; using Repository.Core.Sql; @@ -9,13 +11,35 @@ using Service.Core.User; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddControllers(); +builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = context => + { + var errors = context.ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + + var message = errors.Count == 1 + ? errors[0] + : string.Join(" ", errors); + + var response = new + { + message + }; + + return new BadRequestObjectResult(response); + }; + }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddOpenApi(); // Add FluentValidation builder.Services.AddValidatorsFromAssemblyContaining(); +builder.Services.AddFluentValidationAutoValidation(); // Add health checks builder.Services.AddHealthChecks(); @@ -59,3 +83,6 @@ lifetime.ApplicationStopping.Register(() => }); app.Run(); + +// Make Program class accessible to test projects +public partial class Program { } diff --git a/src/Core/API/API.Specs/Features/Registration.feature b/src/Core/API/API.Specs/Features/Registration.feature index 6140bea..cf6d3d0 100644 --- a/src/Core/API/API.Specs/Features/Registration.feature +++ b/src/Core/API/API.Specs/Features/Registration.feature @@ -52,7 +52,6 @@ Feature: User Registration | Username | FirstName | LastName | Email | DateOfBirth | Password | | newuser | New | User | newuser@example.com | 1990-01-01 | weakpass | Then the response has HTTP status 400 - And the response JSON should have "message" equal "Password does not meet complexity requirements." Scenario: Cannot register a user younger than 19 years of age (regulatory requirement) Given the API is running From 74c5528ea264e0f02e0f8f1a7d4476d151db59eb Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 01:10:04 -0500 Subject: [PATCH 3/7] Format infrastructure dir --- .../Infrastructure.Jwt/IJwtService.cs | 2 +- .../Infrastructure.Jwt.csproj | 26 ++- .../Infrastructure.Jwt/JwtService.cs | 15 +- .../IPasswordService.cs | 2 +- .../Infrastructure.PasswordHashing.csproj | 21 +- .../PasswordService.cs | 14 +- .../Repositories/Auth/AuthRepository.cs | 87 +++++--- .../Repositories/Auth/IAuthRepository.cs | 15 +- .../UserAccount/IUserAccountRepository.cs | 10 +- .../UserAccount/UserAccountRepository.cs | 43 +++- .../Repository.Core/Repository.Core.csproj | 5 +- .../Sql/DefaultSqlConnectionFactory.cs | 12 +- .../Sql/SqlConnectionStringHelper.cs | 30 ++- .../Auth/AuthRepository.test.cs | 176 ++++++++-------- .../Repository.Tests/Repository.Tests.csproj | 17 +- .../UserAccount/UserAccountRepository.test.cs | 193 ++++++++++++------ 16 files changed, 428 insertions(+), 240 deletions(-) diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs index 730a17c..969954a 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs +++ b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs @@ -3,4 +3,4 @@ namespace Service.Core.Jwt; public interface IJwtService { string GenerateJwt(Guid userId, string username, DateTime expiry); -} \ No newline at end of file +} diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj index c7fc3ad..5461560 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj +++ b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj @@ -1,13 +1,19 @@ - - net10.0 - enable - enable - Service.Core.Jwt - + + net10.0 + enable + enable + Service.Core.Jwt + - - - - + + + + diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs index 5e91144..41b2ed1 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs +++ b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs @@ -5,21 +5,27 @@ using Microsoft.IdentityModel.Tokens; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; namespace Service.Core.Jwt; + public class JwtService : IJwtService { - private readonly string? _secret = Environment.GetEnvironmentVariable("JWT_SECRET"); + 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")); + var key = Encoding.UTF8.GetBytes( + _secret ?? throw new InvalidOperationException("secret not set") + ); // Base claims (always present) var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.UniqueName, username), - new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var tokenDescriptor = new SecurityTokenDescriptor @@ -28,7 +34,8 @@ public class JwtService : IJwtService Expires = expiry, SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha256) + SecurityAlgorithms.HmacSha256 + ), }; return handler.CreateToken(tokenDescriptor); diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs index 809fd8b..a5adb4a 100644 --- a/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs @@ -4,4 +4,4 @@ public interface IPasswordService { public string Hash(string password); public bool Verify(string password, string stored); -} \ No newline at end of file +} diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj index acf2e50..23b3af3 100644 --- a/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj @@ -1,12 +1,15 @@ - - net10.0 - enable - enable - Service.Core.Password - + + net10.0 + enable + enable + Service.Core.Password + - - - + + + diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs index c66b47c..5d024de 100644 --- a/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs @@ -19,7 +19,7 @@ public class PasswordService : IPasswordService Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = ArgonMemoryKb, - Iterations = ArgonIterations + Iterations = ArgonIterations, }; var hash = argon2.GetBytes(HashSize); @@ -30,8 +30,12 @@ public class PasswordService : IPasswordService { try { - var parts = stored.Split(':', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length != 2) return false; + var parts = stored.Split( + ':', + StringSplitOptions.RemoveEmptyEntries + ); + if (parts.Length != 2) + return false; var salt = Convert.FromBase64String(parts[0]); var expected = Convert.FromBase64String(parts[1]); @@ -41,7 +45,7 @@ public class PasswordService : IPasswordService Salt = salt, DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), MemorySize = ArgonMemoryKb, - Iterations = ArgonIterations + Iterations = ArgonIterations, }; var actual = argon2.GetBytes(expected.Length); @@ -52,4 +56,4 @@ public class PasswordService : IPasswordService return false; } } -} \ No newline at end of file +} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs index efc3d8a..3b7c536 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs @@ -5,13 +5,12 @@ using Repository.Core.Sql; namespace Repository.Core.Repositories.Auth { - public class AuthRepository : Repository, IAuthRepository + public class AuthRepository + : Repository, + IAuthRepository { public AuthRepository(ISqlConnectionFactory connectionFactory) - : base(connectionFactory) - { - } - + : base(connectionFactory) { } public async Task RegisterUserAsync( string username, @@ -19,7 +18,8 @@ namespace Repository.Core.Repositories.Auth string lastName, string email, DateTime dateOfBirth, - string passwordHash) + string passwordHash + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -45,12 +45,13 @@ namespace Repository.Core.Repositories.Auth LastName = lastName, Email = email, DateOfBirth = dateOfBirth, - CreatedAt = DateTime.UtcNow + CreatedAt = DateTime.UtcNow, }; } - - public async Task GetUserByEmailAsync(string email) + public async Task GetUserByEmailAsync( + string email + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -63,8 +64,9 @@ namespace Repository.Core.Repositories.Auth return await reader.ReadAsync() ? MapToEntity(reader) : null; } - - public async Task GetUserByUsernameAsync(string username) + public async Task GetUserByUsernameAsync( + string username + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -77,7 +79,9 @@ namespace Repository.Core.Repositories.Auth return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public async Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId) + public async Task GetActiveCredentialByUserAccountIdAsync( + Guid userAccountId + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -87,10 +91,15 @@ namespace Repository.Core.Repositories.Auth AddParameter(command, "@UserAccountId", userAccountId); await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() ? MapToCredentialEntity(reader) : null; + return await reader.ReadAsync() + ? MapToCredentialEntity(reader) + : null; } - public async Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash) + public async Task RotateCredentialAsync( + Guid userAccountId, + string newPasswordHash + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -106,11 +115,15 @@ namespace Repository.Core.Repositories.Auth /// /// Maps a data reader row to a UserAccount entity. /// - protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader) + protected override Domain.Core.Entities.UserAccount MapToEntity( + DbDataReader reader + ) { return new Domain.Core.Entities.UserAccount { - UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), + UserAccountId = reader.GetGuid( + reader.GetOrdinal("UserAccountId") + ), Username = reader.GetString(reader.GetOrdinal("Username")), FirstName = reader.GetString(reader.GetOrdinal("FirstName")), LastName = reader.GetString(reader.GetOrdinal("LastName")), @@ -119,10 +132,12 @@ namespace Repository.Core.Repositories.Auth UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), - DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), + DateOfBirth = reader.GetDateTime( + reader.GetOrdinal("DateOfBirth") + ), Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null - : (byte[])reader["Timer"] + : (byte[])reader["Timer"], }; } @@ -133,22 +148,34 @@ namespace Repository.Core.Repositories.Auth { var entity = new UserCredential { - UserCredentialId = reader.GetGuid(reader.GetOrdinal("UserCredentialId")), - UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), + UserCredentialId = reader.GetGuid( + reader.GetOrdinal("UserCredentialId") + ), + UserAccountId = reader.GetGuid( + reader.GetOrdinal("UserAccountId") + ), Hash = reader.GetString(reader.GetOrdinal("Hash")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), }; // Optional columns - var hasTimer = reader.GetSchemaTable()?.Rows - .Cast() - .Any(r => string.Equals(r["ColumnName"]?.ToString(), "Timer", - StringComparison.OrdinalIgnoreCase)) ?? - false; + var hasTimer = + reader + .GetSchemaTable() + ?.Rows.Cast() + .Any(r => + string.Equals( + r["ColumnName"]?.ToString(), + "Timer", + StringComparison.OrdinalIgnoreCase + ) + ) ?? false; if (hasTimer) { - entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null : (byte[])reader["Timer"]; + entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) + ? null + : (byte[])reader["Timer"]; } return entity; @@ -157,7 +184,11 @@ namespace Repository.Core.Repositories.Auth /// /// Helper method to add a parameter to a database command. /// - private static void AddParameter(DbCommand command, string name, object? value) + private static void AddParameter( + DbCommand command, + string name, + object? value + ) { var p = command.CreateParameter(); p.ParameterName = name; diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs index 2f96e96..2ebd49d 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs @@ -24,7 +24,8 @@ namespace Repository.Core.Repositories.Auth string lastName, string email, DateTime dateOfBirth, - string passwordHash); + string passwordHash + ); /// /// Retrieves a user account by email address (typically used for login). @@ -32,7 +33,9 @@ namespace Repository.Core.Repositories.Auth /// /// Email address to search for /// UserAccount if found, null otherwise - Task GetUserByEmailAsync(string email); + Task GetUserByEmailAsync( + string email + ); /// /// Retrieves a user account by username (typically used for login). @@ -40,7 +43,9 @@ namespace Repository.Core.Repositories.Auth /// /// Username to search for /// UserAccount if found, null otherwise - Task GetUserByUsernameAsync(string username); + Task GetUserByUsernameAsync( + string username + ); /// /// Retrieves the active (non-revoked) credential for a user account. @@ -48,7 +53,9 @@ namespace Repository.Core.Repositories.Auth /// /// ID of the user account /// Active UserCredential if found, null otherwise - Task GetActiveCredentialByUserAccountIdAsync(Guid userAccountId); + Task GetActiveCredentialByUserAccountIdAsync( + Guid userAccountId + ); /// /// Rotates a user's credential by invalidating all existing credentials and creating a new one. diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs index d0bed2c..d578bb3 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs @@ -1,15 +1,19 @@ using Domain.Core.Entities; - namespace Repository.Core.Repositories.UserAccount { public interface IUserAccountRepository { Task GetByIdAsync(Guid id); - Task> GetAllAsync(int? limit, int? offset); + Task> GetAllAsync( + int? limit, + int? offset + ); Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount); Task DeleteAsync(Guid id); - Task GetByUsernameAsync(string username); + Task GetByUsernameAsync( + string username + ); Task GetByEmailAsync(string email); } } diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs index 548522b..7b4eb19 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs @@ -6,9 +6,12 @@ using Repository.Core.Sql; namespace Repository.Core.Repositories.UserAccount { public class UserAccountRepository(ISqlConnectionFactory connectionFactory) - : Repository(connectionFactory), IUserAccountRepository + : Repository(connectionFactory), + IUserAccountRepository { - public async Task GetByIdAsync(Guid id) + public async Task GetByIdAsync( + Guid id + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -21,7 +24,9 @@ namespace Repository.Core.Repositories.UserAccount return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public async Task> GetAllAsync(int? limit, int? offset) + public async Task< + IEnumerable + > GetAllAsync(int? limit, int? offset) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -45,7 +50,9 @@ namespace Repository.Core.Repositories.UserAccount return users; } - public async Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount) + public async Task UpdateAsync( + Domain.Core.Entities.UserAccount userAccount + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -73,7 +80,9 @@ namespace Repository.Core.Repositories.UserAccount await command.ExecuteNonQueryAsync(); } - public async Task GetByUsernameAsync(string username) + public async Task GetByUsernameAsync( + string username + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -86,7 +95,9 @@ namespace Repository.Core.Repositories.UserAccount return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public async Task GetByEmailAsync(string email) + public async Task GetByEmailAsync( + string email + ) { await using var connection = await CreateConnection(); await using var command = connection.CreateCommand(); @@ -99,11 +110,15 @@ namespace Repository.Core.Repositories.UserAccount return await reader.ReadAsync() ? MapToEntity(reader) : null; } - protected override Domain.Core.Entities.UserAccount MapToEntity(DbDataReader reader) + protected override Domain.Core.Entities.UserAccount MapToEntity( + DbDataReader reader + ) { return new Domain.Core.Entities.UserAccount { - UserAccountId = reader.GetGuid(reader.GetOrdinal("UserAccountId")), + UserAccountId = reader.GetGuid( + reader.GetOrdinal("UserAccountId") + ), Username = reader.GetString(reader.GetOrdinal("Username")), FirstName = reader.GetString(reader.GetOrdinal("FirstName")), LastName = reader.GetString(reader.GetOrdinal("LastName")), @@ -112,14 +127,20 @@ namespace Repository.Core.Repositories.UserAccount UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), - DateOfBirth = reader.GetDateTime(reader.GetOrdinal("DateOfBirth")), + DateOfBirth = reader.GetDateTime( + reader.GetOrdinal("DateOfBirth") + ), Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) ? null - : (byte[])reader["Timer"] + : (byte[])reader["Timer"], }; } - private static void AddParameter(DbCommand command, string name, object? value) + private static void AddParameter( + DbCommand command, + string name, + object? value + ) { var p = command.CreateParameter(); p.ParameterName = name; diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj index 1a3107a..9268fc3 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj @@ -12,7 +12,10 @@ Version="160.1000.6" /> - + diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs index b8cdec6..e12e1b1 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs @@ -2,17 +2,21 @@ using System.Data.Common; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; - namespace Repository.Core.Sql { - public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory + public class DefaultSqlConnectionFactory(IConfiguration configuration) + : ISqlConnectionFactory { - private readonly string _connectionString = GetConnectionString(configuration); + 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"); + var fullConnectionString = Environment.GetEnvironmentVariable( + "DB_CONNECTION_STRING" + ); if (!string.IsNullOrEmpty(fullConnectionString)) { return fullConnectionString; diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs index 7e50707..42a991c 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs @@ -12,18 +12,30 @@ namespace Repository.Core.Sql /// 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 server = + Environment.GetEnvironmentVariable("DB_SERVER") + ?? throw new InvalidOperationException( + "DB_SERVER environment variable is not set" + ); - var dbName = databaseName + var dbName = + databaseName ?? Environment.GetEnvironmentVariable("DB_NAME") - ?? throw new InvalidOperationException("DB_NAME environment variable is not set"); + ?? 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 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 password = + Environment.GetEnvironmentVariable("DB_PASSWORD") + ?? throw new InvalidOperationException( + "DB_PASSWORD environment variable is not set" + ); var builder = new SqlConnectionStringBuilder { @@ -32,7 +44,7 @@ namespace Repository.Core.Sql UserID = user, Password = password, TrustServerCertificate = true, - Encrypt = true + Encrypt = true, }; return builder.ConnectionString; diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs index 91b6d4a..efd8eac 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs @@ -1,15 +1,15 @@ -using Apps72.Dev.Data.DbMocker; -using Repository.Core.Repositories.Auth; -using FluentAssertions; -using Repository.Tests.Database; using System.Data; +using Apps72.Dev.Data.DbMocker; +using FluentAssertions; +using Repository.Core.Repositories.Auth; +using Repository.Tests.Database; namespace Repository.Tests.Auth; public class AuthRepositoryTest { - private static AuthRepository CreateRepo(MockDbConnection conn) - => new(new TestConnectionFactory(conn)); + private static AuthRepository CreateRepo(MockDbConnection conn) => + new(new TestConnectionFactory(conn)); [Fact] public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount() @@ -17,10 +17,12 @@ public class AuthRepositoryTest var expectedUserId = Guid.NewGuid(); var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "USP_RegisterUser") - .ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid))) - .AddRow(expectedUserId)); + conn.Mocks.When(cmd => cmd.CommandText == "USP_RegisterUser") + .ReturnsTable( + MockTable + .WithColumns(("UserAccountId", typeof(Guid))) + .AddRow(expectedUserId) + ); var repo = CreateRepo(conn); var result = await repo.RegisterUserAsync( @@ -47,29 +49,32 @@ public class AuthRepositoryTest var userId = Guid.NewGuid(); var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") - .ReturnsTable(MockTable.WithColumns( - ("UserAccountId", typeof(Guid)), - ("Username", typeof(string)), - ("FirstName", typeof(string)), - ("LastName", typeof(string)), - ("Email", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("UpdatedAt", typeof(DateTime?)), - ("DateOfBirth", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow( - userId, - "emailuser", - "Email", - "User", - "emailuser@example.com", - DateTime.UtcNow, - null, - new DateTime(1990, 5, 15), - null - )); + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") + .ReturnsTable( + MockTable + .WithColumns( + ("UserAccountId", typeof(Guid)), + ("Username", typeof(string)), + ("FirstName", typeof(string)), + ("LastName", typeof(string)), + ("Email", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("UpdatedAt", typeof(DateTime?)), + ("DateOfBirth", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + userId, + "emailuser", + "Email", + "User", + "emailuser@example.com", + DateTime.UtcNow, + null, + new DateTime(1990, 5, 15), + null + ) + ); var repo = CreateRepo(conn); var result = await repo.GetUserByEmailAsync("emailuser@example.com"); @@ -87,8 +92,7 @@ public class AuthRepositoryTest { var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") .ReturnsTable(MockTable.Empty()); var repo = CreateRepo(conn); @@ -103,29 +107,34 @@ public class AuthRepositoryTest var userId = Guid.NewGuid(); var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") - .ReturnsTable(MockTable.WithColumns( - ("UserAccountId", typeof(Guid)), - ("Username", typeof(string)), - ("FirstName", typeof(string)), - ("LastName", typeof(string)), - ("Email", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("UpdatedAt", typeof(DateTime?)), - ("DateOfBirth", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow( - userId, - "usernameuser", - "Username", - "User", - "username@example.com", - DateTime.UtcNow, - null, - new DateTime(1985, 8, 20), - null - )); + conn.Mocks.When(cmd => + cmd.CommandText == "usp_GetUserAccountByUsername" + ) + .ReturnsTable( + MockTable + .WithColumns( + ("UserAccountId", typeof(Guid)), + ("Username", typeof(string)), + ("FirstName", typeof(string)), + ("LastName", typeof(string)), + ("Email", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("UpdatedAt", typeof(DateTime?)), + ("DateOfBirth", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + userId, + "usernameuser", + "Username", + "User", + "username@example.com", + DateTime.UtcNow, + null, + new DateTime(1985, 8, 20), + null + ) + ); var repo = CreateRepo(conn); var result = await repo.GetUserByUsernameAsync("usernameuser"); @@ -141,8 +150,9 @@ public class AuthRepositoryTest { var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") + conn.Mocks.When(cmd => + cmd.CommandText == "usp_GetUserAccountByUsername" + ) .ReturnsTable(MockTable.Empty()); var repo = CreateRepo(conn); @@ -158,21 +168,26 @@ public class AuthRepositoryTest var credentialId = Guid.NewGuid(); var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") - .ReturnsTable(MockTable.WithColumns( - ("UserCredentialId", typeof(Guid)), - ("UserAccountId", typeof(Guid)), - ("Hash", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow( - credentialId, - userId, - "hashed_password_value", - DateTime.UtcNow, - null - )); + conn.Mocks.When(cmd => + cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId" + ) + .ReturnsTable( + MockTable + .WithColumns( + ("UserCredentialId", typeof(Guid)), + ("UserAccountId", typeof(Guid)), + ("Hash", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + credentialId, + userId, + "hashed_password_value", + DateTime.UtcNow, + null + ) + ); var repo = CreateRepo(conn); var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId); @@ -189,8 +204,9 @@ public class AuthRepositoryTest var userId = Guid.NewGuid(); var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId") + conn.Mocks.When(cmd => + cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId" + ) .ReturnsTable(MockTable.Empty()); var repo = CreateRepo(conn); @@ -206,14 +222,14 @@ public class AuthRepositoryTest var newPasswordHash = "new_hashed_password"; var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "USP_RotateUserCredential") + conn.Mocks.When(cmd => cmd.CommandText == "USP_RotateUserCredential") .ReturnsScalar(1); var repo = CreateRepo(conn); // Should not throw - var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash); + var act = async () => + await repo.RotateCredentialAsync(userId, newPasswordHash); await act.Should().NotThrowAsync(); } } diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj index a1d64e0..d3755e2 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj @@ -15,9 +15,18 @@ - - - + + + @@ -28,4 +37,4 @@ - \ No newline at end of file + diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs index 5cf9a64..65bdf6c 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs @@ -1,38 +1,50 @@ using Apps72.Dev.Data.DbMocker; -using Repository.Core.Repositories.UserAccount; using FluentAssertions; +using Repository.Core.Repositories.UserAccount; using Repository.Tests.Database; namespace Repository.Tests.UserAccount; public class UserAccountRepositoryTest { - private static UserAccountRepository CreateRepo(MockDbConnection conn) - => new(new TestConnectionFactory(conn)); + private static UserAccountRepository CreateRepo(MockDbConnection conn) => + new(new TestConnectionFactory(conn)); [Fact] public async Task GetByIdAsync_ReturnsRow_Mapped() { var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountById") - .ReturnsTable(MockTable.WithColumns( - ("UserAccountId", typeof(Guid)), - ("Username", typeof(string)), - ("FirstName", typeof(string)), - ("LastName", typeof(string)), - ("Email", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("UpdatedAt", typeof(DateTime?)), - ("DateOfBirth", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), - "yerb", "Aaron", "Po", "aaronpo@example.com", - new DateTime(2020, 1, 1), null, - new DateTime(1990, 1, 1), null)); + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountById") + .ReturnsTable( + MockTable + .WithColumns( + ("UserAccountId", typeof(Guid)), + ("Username", typeof(string)), + ("FirstName", typeof(string)), + ("LastName", typeof(string)), + ("Email", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("UpdatedAt", typeof(DateTime?)), + ("DateOfBirth", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), + "yerb", + "Aaron", + "Po", + "aaronpo@example.com", + new DateTime(2020, 1, 1), + null, + new DateTime(1990, 1, 1), + null + ) + ); var repo = CreateRepo(conn); - var result = await repo.GetByIdAsync(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")); + var result = await repo.GetByIdAsync( + Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + ); result.Should().NotBeNull(); result!.Username.Should().Be("yerb"); @@ -43,48 +55,85 @@ public class UserAccountRepositoryTest public async Task GetAllAsync_ReturnsMultipleRows() { var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetAllUserAccounts") - .ReturnsTable(MockTable.WithColumns( - ("UserAccountId", typeof(Guid)), - ("Username", typeof(string)), - ("FirstName", typeof(string)), - ("LastName", typeof(string)), - ("Email", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("UpdatedAt", typeof(DateTime?)), - ("DateOfBirth", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow(Guid.NewGuid(), "a", "A", "A", "a@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, - null) - .AddRow(Guid.NewGuid(), "b", "B", "B", "b@example.com", DateTime.UtcNow, null, DateTime.UtcNow.Date, - null)); + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetAllUserAccounts") + .ReturnsTable( + MockTable + .WithColumns( + ("UserAccountId", typeof(Guid)), + ("Username", typeof(string)), + ("FirstName", typeof(string)), + ("LastName", typeof(string)), + ("Email", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("UpdatedAt", typeof(DateTime?)), + ("DateOfBirth", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + Guid.NewGuid(), + "a", + "A", + "A", + "a@example.com", + DateTime.UtcNow, + null, + DateTime.UtcNow.Date, + null + ) + .AddRow( + Guid.NewGuid(), + "b", + "B", + "B", + "b@example.com", + DateTime.UtcNow, + null, + DateTime.UtcNow.Date, + null + ) + ); var repo = CreateRepo(conn); var results = (await repo.GetAllAsync(null, null)).ToList(); results.Should().HaveCount(2); - results.Select(r => r.Username).Should().BeEquivalentTo(new[] { "a", "b" }); + results + .Select(r => r.Username) + .Should() + .BeEquivalentTo(new[] { "a", "b" }); } - [Fact] public async Task GetByUsername_ReturnsRow() { var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername") - .ReturnsTable(MockTable.WithColumns( - ("UserAccountId", typeof(Guid)), - ("Username", typeof(string)), - ("FirstName", typeof(string)), - ("LastName", typeof(string)), - ("Email", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("UpdatedAt", typeof(DateTime?)), - ("DateOfBirth", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow(Guid.NewGuid(), "lookupuser", "L", "U", "lookup@example.com", DateTime.UtcNow, null, - DateTime.UtcNow.Date, null)); + conn.Mocks.When(cmd => + cmd.CommandText == "usp_GetUserAccountByUsername" + ) + .ReturnsTable( + MockTable + .WithColumns( + ("UserAccountId", typeof(Guid)), + ("Username", typeof(string)), + ("FirstName", typeof(string)), + ("LastName", typeof(string)), + ("Email", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("UpdatedAt", typeof(DateTime?)), + ("DateOfBirth", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + Guid.NewGuid(), + "lookupuser", + "L", + "U", + "lookup@example.com", + DateTime.UtcNow, + null, + DateTime.UtcNow.Date, + null + ) + ); var repo = CreateRepo(conn); var result = await repo.GetByUsernameAsync("lookupuser"); @@ -96,20 +145,32 @@ public class UserAccountRepositoryTest public async Task GetByEmail_ReturnsRow() { var conn = new MockDbConnection(); - conn.Mocks - .When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") - .ReturnsTable(MockTable.WithColumns( - ("UserAccountId", typeof(Guid)), - ("Username", typeof(string)), - ("FirstName", typeof(string)), - ("LastName", typeof(string)), - ("Email", typeof(string)), - ("CreatedAt", typeof(DateTime)), - ("UpdatedAt", typeof(DateTime?)), - ("DateOfBirth", typeof(DateTime)), - ("Timer", typeof(byte[])) - ).AddRow(Guid.NewGuid(), "byemail", "B", "E", "byemail@example.com", DateTime.UtcNow, null, - DateTime.UtcNow.Date, null)); + conn.Mocks.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail") + .ReturnsTable( + MockTable + .WithColumns( + ("UserAccountId", typeof(Guid)), + ("Username", typeof(string)), + ("FirstName", typeof(string)), + ("LastName", typeof(string)), + ("Email", typeof(string)), + ("CreatedAt", typeof(DateTime)), + ("UpdatedAt", typeof(DateTime?)), + ("DateOfBirth", typeof(DateTime)), + ("Timer", typeof(byte[])) + ) + .AddRow( + Guid.NewGuid(), + "byemail", + "B", + "E", + "byemail@example.com", + DateTime.UtcNow, + null, + DateTime.UtcNow.Date, + null + ) + ); var repo = CreateRepo(conn); var result = await repo.GetByEmailAsync("byemail@example.com"); From a038a12fcac9d5e2bc9cad229ed0cb884aac1697 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 09:54:39 -0500 Subject: [PATCH 4/7] Update root namespaces --- src/Core/API/API.Core/Controllers/AuthController.cs | 2 +- src/Core/API/API.Core/Program.cs | 6 +++--- src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs | 2 +- .../Infrastructure.Jwt/Infrastructure.Jwt.csproj | 2 +- src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs | 2 +- .../{PasswordService.cs => Argon2Infrastructure.cs} | 4 ++-- .../{IPasswordService.cs => IPasswordInfra.cs} | 4 ++-- .../Infrastructure.PasswordHashing.csproj | 2 +- src/Core/Service/Service.Core/Auth/AuthService.cs | 8 ++++---- 9 files changed, 16 insertions(+), 16 deletions(-) rename src/Core/Infrastructure/Infrastructure.PasswordHashing/{PasswordService.cs => Argon2Infrastructure.cs} (94%) rename src/Core/Infrastructure/Infrastructure.PasswordHashing/{IPasswordService.cs => IPasswordInfra.cs} (57%) diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs index 0f22ea5..91440ff 100644 --- a/src/Core/API/API.Core/Controllers/AuthController.cs +++ b/src/Core/API/API.Core/Controllers/AuthController.cs @@ -1,9 +1,9 @@ using API.Core.Contracts.Auth; using API.Core.Contracts.Common; using Domain.Core.Entities; +using Infrastructure.Jwt; using Microsoft.AspNetCore.Mvc; using Service.Core.Auth; -using Service.Core.Jwt; namespace API.Core.Controllers { diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index b4def9f..33e20d6 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -1,12 +1,12 @@ using FluentValidation; using FluentValidation.AspNetCore; +using Infrastructure.Jwt; +using Infrastructure.PasswordHashing; using Microsoft.AspNetCore.Mvc; using Repository.Core.Repositories.Auth; using Repository.Core.Repositories.UserAccount; using Repository.Core.Sql; using Service.Core.Auth; -using Service.Core.Jwt; -using Service.Core.Password; using Service.Core.User; var builder = WebApplication.CreateBuilder(args); @@ -59,7 +59,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs index 969954a..b62d2e3 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs +++ b/src/Core/Infrastructure/Infrastructure.Jwt/IJwtService.cs @@ -1,4 +1,4 @@ -namespace Service.Core.Jwt; +namespace Infrastructure.Jwt; public interface IJwtService { diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj index 5461560..cddd219 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj +++ b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj @@ -3,7 +3,7 @@ net10.0 enable enable - Service.Core.Jwt + Infrastructure.Jwt diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs index 41b2ed1..d0fba84 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs +++ b/src/Core/Infrastructure/Infrastructure.Jwt/JwtService.cs @@ -4,7 +4,7 @@ using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; -namespace Service.Core.Jwt; +namespace Infrastructure.Jwt; public class JwtService : IJwtService { diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs similarity index 94% rename from src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs rename to src/Core/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs index 5d024de..ff47a77 100644 --- a/src/Core/Infrastructure/Infrastructure.PasswordHashing/PasswordService.cs +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Argon2Infrastructure.cs @@ -2,9 +2,9 @@ using System.Security.Cryptography; using System.Text; using Konscious.Security.Cryptography; -namespace Service.Core.Password; +namespace Infrastructure.PasswordHashing; -public class PasswordService : IPasswordService +public class Argon2Infrastructure : IPasswordInfra { private const int SaltSize = 16; // 128-bit private const int HashSize = 32; // 256-bit diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfra.cs similarity index 57% rename from src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs rename to src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfra.cs index a5adb4a..9a2df0b 100644 --- a/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordService.cs +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/IPasswordInfra.cs @@ -1,6 +1,6 @@ -namespace Service.Core.Password; +namespace Infrastructure.PasswordHashing; -public interface IPasswordService +public interface IPasswordInfra { public string Hash(string password); public bool Verify(string password, string stored); diff --git a/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj index 23b3af3..97afa1f 100644 --- a/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj +++ b/src/Core/Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj @@ -3,7 +3,7 @@ net10.0 enable enable - Service.Core.Password + Infrastructure.PasswordHashing diff --git a/src/Core/Service/Service.Core/Auth/AuthService.cs b/src/Core/Service/Service.Core/Auth/AuthService.cs index 9d13629..51dbbc8 100644 --- a/src/Core/Service/Service.Core/Auth/AuthService.cs +++ b/src/Core/Service/Service.Core/Auth/AuthService.cs @@ -1,12 +1,12 @@ using Domain.Core.Entities; +using Infrastructure.PasswordHashing; using Repository.Core.Repositories.Auth; -using Service.Core.Password; namespace Service.Core.Auth; public class AuthService( IAuthRepository authRepo, - IPasswordService passwordService + IPasswordInfra passwordInfra ) : IAuthService { public async Task RegisterAsync(UserAccount userAccount, string password) @@ -19,7 +19,7 @@ public class AuthService( } // password hashing - var hashed = passwordService.Hash(password); + var hashed = passwordInfra.Hash(password); // Register user with hashed password return await authRepo.RegisterUserAsync( @@ -43,6 +43,6 @@ public class AuthService( var activeCred = await authRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId); if (activeCred is null) return null; - return !passwordService.Verify(password, activeCred.Hash) ? null : user; + return !passwordInfra.Verify(password, activeCred.Hash) ? null : user; } } From 4f92741b4f34fbfec77c0ddeefbf003c787e1e70 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 10:21:34 -0500 Subject: [PATCH 5/7] Refactor repository structure --- .gitignore | 3 - README.md | 99 ++++----- docker-compose.test.yaml | 2 +- src/Core/API/API.Core/API.Core.csproj | 3 +- src/Core/API/API.Core/Dockerfile | 2 +- src/Core/API/API.Core/Program.cs | 6 +- src/Core/API/API.Specs/Dockerfile | 2 +- src/Core/Core.slnx | 8 +- .../Database.Seed/Database.Seed.csproj | 3 +- src/Core/Database/Database.Seed/UserSeeder.cs | 1 - .../Auth/AuthRepository.test.cs | 2 +- .../Database/TestConnectionFactory.cs | 11 + .../Dockerfile | 15 ++ .../Repository.Tests.csproj | 4 +- .../UserAccount/UserAccountRepository.test.cs | 2 +- .../Auth/AuthRepository.cs | 198 +++++++++++++++++ .../Auth/IAuthRepository.cs | 67 ++++++ ...sproj => Infrastructure.Repository.csproj} | 4 +- .../Repositories/Auth/AuthRepository.cs | 199 ------------------ .../Repositories/Auth/IAuthRepository.cs | 68 ------ .../Repositories/Repository.cs | 18 -- .../UserAccount/IUserAccountRepository.cs | 19 -- .../UserAccount/UserAccountRepository.cs | 151 ------------- .../Sql/DefaultSqlConnectionFactory.cs | 50 ----- .../Sql/ISqlConnectionFactory.cs | 9 - .../Sql/SqlConnectionStringHelper.cs | 62 ------ .../Repository.Tests/Dockerfile | 15 -- .../Infrastructure.Repository/Repository.cs | 17 ++ .../Sql/DefaultSqlConnectionFactory.cs | 49 +++++ .../Sql/ISqlConnectionFactory.cs | 8 + .../Sql/SqlConnectionStringHelper.cs | 61 ++++++ .../UserAccount/IUserAccountRepository.cs | 16 ++ .../UserAccount/UserAccountRepository.cs | 149 +++++++++++++ .../Service/Service.Core/Auth/AuthService.cs | 2 +- .../Service/Service.Core/Service.Core.csproj | 3 +- .../Service/Service.Core/User/UserService.cs | 2 +- 36 files changed, 651 insertions(+), 679 deletions(-) rename src/Core/Infrastructure/{Infrastructure.Repository/Repository.Tests => Infrastructure.Repository.Tests}/Auth/AuthRepository.test.cs (99%) create mode 100644 src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile rename src/Core/Infrastructure/{Infrastructure.Repository/Repository.Tests => Infrastructure.Repository.Tests}/Repository.Tests.csproj (88%) rename src/Core/Infrastructure/{Infrastructure.Repository/Repository.Tests => Infrastructure.Repository.Tests}/UserAccount/UserAccountRepository.test.cs (99%) create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs rename src/Core/Infrastructure/Infrastructure.Repository/{Repository.Core/Repository.Core.csproj => Infrastructure.Repository.csproj} (83%) delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Repository.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/ISqlConnectionFactory.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs delete mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Repository.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs create mode 100644 src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs diff --git a/.gitignore b/.gitignore index ecede2b..2dc761b 100644 --- a/.gitignore +++ b/.gitignore @@ -483,9 +483,6 @@ FodyWeavers.xsd *.feature.cs - -database - .env .env.dev .env.test diff --git a/README.md b/README.md index f1d8087..5304939 100644 --- a/README.md +++ b/README.md @@ -2,46 +2,6 @@ A social platform for craft beer enthusiasts to discover breweries, share reviews, and connect with fellow beer lovers. - -## Table of Contents - -- [Project Status](#project-status) -- [Repository Structure](#repository-structure) -- [Technology Stack](#technology-stack) -- [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Quick Start (Development Environment)](#quick-start-development-environment) - - [Manual Setup (Without Docker)](#manual-setup-without-docker) -- [Environment Variables](#environment-variables) - - [Overview](#overview) - - [Backend Variables (.NET API)](#backend-variables-net-api) - - [Frontend Variables (Next.js)](#frontend-variables-nextjs) - - [Docker Variables](#docker-variables) - - [External Services](#external-services) - - [Generating Secrets](#generating-secrets) - - [Environment File Structure](#environment-file-structure) - - [Variable Reference Table](#variable-reference-table) -- [Testing](#testing) -- [Database Schema](#database-schema) -- [Authentication & Security](#authentication--security) -- [Architecture Patterns](#architecture-patterns) -- [Docker & Containerization](#docker--containerization) - - [Container Architecture](#container-architecture) - - [Docker Compose Environments](#docker-compose-environments) - - [Service Dependencies](#service-dependencies) - - [Health Checks](#health-checks) - - [Volumes](#volumes) - - [Networks](#networks) - - [Environment Variables](#environment-variables) - - [Container Lifecycle](#container-lifecycle) -- [Docker Tips & Troubleshooting](#docker-tips--troubleshooting) -- [Roadmap](#roadmap) -- [License](#license) -- [Contact & Support](#contact--support) - ---- - - ## Project Status This project is in active development, transitioning from a full-stack Next.js application to a **multi-project monorepo** with: @@ -49,10 +9,10 @@ This project is in active development, transitioning from a full-stack Next.js a - **Frontend**: Next.js with TypeScript - **Architecture**: SQL-first approach using stored procedures -**Current State** (February 2025): +**Current State** (February 2026): - Core authentication and user management APIs functional - Database schema and migrations established -- Repository and service layers implemented +- Domain, Infrastructure, Repository, and Service layers implemented - Frontend integration with .NET API in progress - Migrating remaining features from Next.js serverless functions @@ -65,18 +25,26 @@ This project is in active development, transitioning from a full-stack Next.js a ``` src/Core/ ├── API/ -│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI -│ └── API.Specs/ # Integration tests using Reqnroll (BDD) +│ ├── API.Core/ # ASP.NET Core Web API with Swagger/OpenAPI +│ └── API.Specs/ # Integration tests using Reqnroll (BDD) ├── Database/ -│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts) -│ └── Database.Seed/ # Database seeding for development/testing -├── Repository/ -│ ├── Repository.Core/ # Data access layer (stored procedure-based) -│ └── Repository.Tests/ # Unit tests for repositories +│ ├── Database.Migrations/ # DbUp migrations (embedded SQL scripts) +│ └── Database.Seed/ # Database seeding for development/testing +├── Domain/ +│ └── Domain.csproj # Domain entities and models +│ └── Entities/ # Core domain entities (UserAccount, UserCredential, etc.) +├── Infrastructure/ +│ ├── Infrastructure.Jwt/ # JWT token generation and validation +│ ├── Infrastructure.PasswordHashing/ # Argon2id password hashing +│ └── Infrastructure.Repository/ +│ ├── Infrastructure.Repository/ # Data access layer (stored procedure-based) +│ └── Infrastructure.Repository.Tests/ # Unit tests for repositories └── Service/ - └── Service.Core/ # Business logic layer + └── Service.Core/ # Business logic layer -Website/ # Next.js frontend application +Website/ # Next.js frontend application +misc/ +└── raw-data/ # Sample data files (breweries, beers) ``` ### Key Components @@ -86,6 +54,7 @@ Website/ # Next.js frontend application - Controllers: `AuthController`, `UserController` - Configured with Swagger UI for API exploration - Health checks and structured logging +- Middleware for error handling and request processing **Database Layer** - SQL Server with stored procedures for all data operations @@ -93,7 +62,19 @@ Website/ # Next.js frontend application - Comprehensive schema including users, breweries, beers, locations, and social features - Seeders for development data (users, locations across US/Canada/Mexico) -**Repository Layer** (`Repository.Core`) +**Domain Layer** (`Domain`) +- Core business entities and models +- Entities: `UserAccount`, `UserCredential`, `UserVerification` +- Shared domain logic and value objects +- No external dependencies - pure domain model + +**Infrastructure Layer** +- **Infrastructure.Jwt**: JWT token generation, validation, and configuration +- **Infrastructure.PasswordHashing**: Argon2id password hashing with configurable parameters +- **Infrastructure.Password**: Password utilities and validation +- **Infrastructure.Repository**: Repository pattern infrastructure and base classes + +**Repository Layer** (`Infrastructure.Repository`) - Abstraction over SQL Server using ADO.NET - `ISqlConnectionFactory` for connection management - Repositories: `AuthRepository`, `UserAccountRepository` @@ -101,9 +82,9 @@ Website/ # Next.js frontend application **Service Layer** (`Service.Core`) - Business logic and orchestration -- Services: `AuthService`, `UserService`, `JwtService` -- Password hashing with Argon2id -- JWT token generation +- Services: `AuthService`, `UserService` +- Integration with infrastructure components +- Transaction management and business rule enforcement **Frontend** (`Website`) - Next.js 14+ with TypeScript @@ -334,7 +315,7 @@ Provide a complete SQL Server connection string: DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" ``` -The connection factory checks for `DB_CONNECTION_STRING` first, then falls back to building from components. See [DefaultSqlConnectionFactory.cs](src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs). +The connection factory checks for `DB_CONNECTION_STRING` first, then falls back to building from components. See [DefaultSqlConnectionFactory.cs](src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs). #### JWT Authentication @@ -578,7 +559,7 @@ docker compose -f docker-compose.test.yaml up --abort-on-container-exit This runs: - **API.Specs** - BDD integration tests -- **Repository.Tests** - Unit tests for data access +- **Infrastructure.Repository.Tests** - Unit tests for data access Test results are output to `./test-results/`. @@ -590,10 +571,10 @@ cd src/Core dotnet test API/API.Specs/API.Specs.csproj ``` -**Unit Tests (Repository.Tests)** +**Unit Tests (Infrastructure.Repository.Tests)** ```bash cd src/Core -dotnet test Repository/Repository.Tests/Repository.Tests.csproj +dotnet test Infrastructure/Infrastructure.Repository/Infrastructure.Repository.Tests/Repository.Tests.csproj ``` ### Test Features diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 4da36ef..06aff2f 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -101,7 +101,7 @@ services: condition: service_completed_successfully build: context: ./src/Core - dockerfile: Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile + dockerfile: Infrastructure/Infrastructure.Repository.Tests/Dockerfile args: BUILD_CONFIGURATION: Release environment: diff --git a/src/Core/API/API.Core/API.Core.csproj b/src/Core/API/API.Core/API.Core.csproj index 79d1cc4..4ddfa09 100644 --- a/src/Core/API/API.Core/API.Core.csproj +++ b/src/Core/API/API.Core/API.Core.csproj @@ -19,8 +19,7 @@ - + diff --git a/src/Core/API/API.Core/Dockerfile b/src/Core/API/API.Core/Dockerfile index 0d7c7e1..cd61359 100644 --- a/src/Core/API/API.Core/Dockerfile +++ b/src/Core/API/API.Core/Dockerfile @@ -10,7 +10,7 @@ ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"] COPY ["Domain/Domain.csproj", "Domain/"] -COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"] +COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"] COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"] COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"] COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"] diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index 33e20d6..640dbb6 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -2,10 +2,10 @@ using FluentValidation; using FluentValidation.AspNetCore; using Infrastructure.Jwt; using Infrastructure.PasswordHashing; +using Infrastructure.Repository.Auth; +using Infrastructure.Repository.Sql; +using Infrastructure.Repository.UserAccount; using Microsoft.AspNetCore.Mvc; -using Repository.Core.Repositories.Auth; -using Repository.Core.Repositories.UserAccount; -using Repository.Core.Sql; using Service.Core.Auth; using Service.Core.User; diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile index 2324cf4..b934157 100644 --- a/src/Core/API/API.Specs/Dockerfile +++ b/src/Core/API/API.Specs/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /src COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"] COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"] COPY ["Domain/Domain.csproj", "Domain/"] -COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"] +COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"] COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"] COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"] COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"] diff --git a/src/Core/Core.slnx b/src/Core/Core.slnx index 00eb33e..293b22b 100644 --- a/src/Core/Core.slnx +++ b/src/Core/Core.slnx @@ -12,11 +12,9 @@ - - - + + + diff --git a/src/Core/Database/Database.Seed/Database.Seed.csproj b/src/Core/Database/Database.Seed/Database.Seed.csproj index 0deeae4..9a44208 100644 --- a/src/Core/Database/Database.Seed/Database.Seed.csproj +++ b/src/Core/Database/Database.Seed/Database.Seed.csproj @@ -19,7 +19,6 @@ - + diff --git a/src/Core/Database/Database.Seed/UserSeeder.cs b/src/Core/Database/Database.Seed/UserSeeder.cs index 13465b9..3dfa52c 100644 --- a/src/Core/Database/Database.Seed/UserSeeder.cs +++ b/src/Core/Database/Database.Seed/UserSeeder.cs @@ -2,7 +2,6 @@ using System.Data; using System.Security.Cryptography; using System.Text; using Domain.Core.Entities; -using Repository.Core.Repositories; using idunno.Password; using Konscious.Security.Cryptography; using Microsoft.Data.SqlClient; diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs similarity index 99% rename from src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs index efd8eac..f8bf9d8 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Auth/AuthRepository.test.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Auth/AuthRepository.test.cs @@ -1,7 +1,7 @@ using System.Data; using Apps72.Dev.Data.DbMocker; using FluentAssertions; -using Repository.Core.Repositories.Auth; +using Infrastructure.Repository.Auth; using Repository.Tests.Database; namespace Repository.Tests.Auth; diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs new file mode 100644 index 0000000..f455382 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Database/TestConnectionFactory.cs @@ -0,0 +1,11 @@ +using System.Data.Common; +using Infrastructure.Repository.Sql; + +namespace Repository.Tests.Database; + +internal class TestConnectionFactory(DbConnection conn) : ISqlConnectionFactory +{ + private readonly DbConnection _conn = conn; + + public DbConnection CreateConnection() => _conn; +} diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile new file mode 100644 index 0000000..2013eb1 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Domain/Domain.csproj", "Domain/"] +COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"] +COPY ["Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository.Tests/"] +RUN dotnet restore "Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj" +COPY . . +WORKDIR "/src/Infrastructure/Infrastructure.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/Infrastructure/Infrastructure.Repository.Tests +ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"] diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj similarity index 88% rename from src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj index d3755e2..a71f674 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj +++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj @@ -4,7 +4,7 @@ enable enable false - Repository.Tests + Infrastructure.Repository.Tests @@ -35,6 +35,6 @@ - + diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs b/src/Core/Infrastructure/Infrastructure.Repository.Tests/UserAccount/UserAccountRepository.test.cs similarity index 99% rename from src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/UserAccount/UserAccountRepository.test.cs index 65bdf6c..d395658 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/UserAccount/UserAccountRepository.test.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository.Tests/UserAccount/UserAccountRepository.test.cs @@ -1,6 +1,6 @@ using Apps72.Dev.Data.DbMocker; using FluentAssertions; -using Repository.Core.Repositories.UserAccount; +using Infrastructure.Repository.UserAccount; using Repository.Tests.Database; namespace Repository.Tests.UserAccount; diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs new file mode 100644 index 0000000..65a5501 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs @@ -0,0 +1,198 @@ +using System.Data; +using System.Data.Common; +using Domain.Core.Entities; +using Infrastructure.Repository.Sql; + +namespace Infrastructure.Repository.Auth; + +public class AuthRepository + : Repository, + IAuthRepository +{ + public AuthRepository(ISqlConnectionFactory connectionFactory) + : base(connectionFactory) { } + + public async Task RegisterUserAsync( + string username, + string firstName, + string lastName, + string email, + DateTime dateOfBirth, + string passwordHash + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + + command.CommandText = "USP_RegisterUser"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@Username", username); + AddParameter(command, "@FirstName", firstName); + AddParameter(command, "@LastName", lastName); + AddParameter(command, "@Email", email); + AddParameter(command, "@DateOfBirth", dateOfBirth); + AddParameter(command, "@Hash", passwordHash); + + var result = await command.ExecuteScalarAsync(); + var userAccountId = result != null ? (Guid)result : Guid.Empty; + + return new Domain.Core.Entities.UserAccount + { + UserAccountId = userAccountId, + Username = username, + FirstName = firstName, + LastName = lastName, + Email = email, + DateOfBirth = dateOfBirth, + CreatedAt = DateTime.UtcNow, + }; + } + + public async Task GetUserByEmailAsync( + string email + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_GetUserAccountByEmail"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@Email", email); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + public async Task GetUserByUsernameAsync( + string username + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_GetUserAccountByUsername"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@Username", username); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + public async Task GetActiveCredentialByUserAccountIdAsync( + Guid userAccountId + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "USP_GetActiveUserCredentialByUserAccountId"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@UserAccountId", userAccountId); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() + ? MapToCredentialEntity(reader) + : null; + } + + public async Task RotateCredentialAsync( + Guid userAccountId, + string newPasswordHash + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "USP_RotateUserCredential"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@UserAccountId_", userAccountId); + AddParameter(command, "@Hash", newPasswordHash); + + await command.ExecuteNonQueryAsync(); + } + + /// + /// Maps a data reader row to a UserAccount entity. + /// + protected override Domain.Core.Entities.UserAccount MapToEntity( + DbDataReader reader + ) + { + return new Domain.Core.Entities.UserAccount + { + UserAccountId = reader.GetGuid( + reader.GetOrdinal("UserAccountId") + ), + Username = reader.GetString(reader.GetOrdinal("Username")), + FirstName = reader.GetString(reader.GetOrdinal("FirstName")), + LastName = reader.GetString(reader.GetOrdinal("LastName")), + Email = reader.GetString(reader.GetOrdinal("Email")), + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), + UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) + ? null + : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), + DateOfBirth = reader.GetDateTime( + reader.GetOrdinal("DateOfBirth") + ), + Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) + ? null + : (byte[])reader["Timer"], + }; + } + + /// + /// Maps a data reader row to a UserCredential entity. + /// + private static UserCredential MapToCredentialEntity(DbDataReader reader) + { + var entity = new UserCredential + { + UserCredentialId = reader.GetGuid( + reader.GetOrdinal("UserCredentialId") + ), + UserAccountId = reader.GetGuid( + reader.GetOrdinal("UserAccountId") + ), + Hash = reader.GetString(reader.GetOrdinal("Hash")), + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), + }; + + // Optional columns + var hasTimer = + reader + .GetSchemaTable() + ?.Rows.Cast() + .Any(r => + string.Equals( + r["ColumnName"]?.ToString(), + "Timer", + StringComparison.OrdinalIgnoreCase + ) + ) ?? false; + + if (hasTimer) + { + entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) + ? null + : (byte[])reader["Timer"]; + } + + return entity; + } + + /// + /// Helper method to add a parameter to a database command. + /// + private static void AddParameter( + DbCommand command, + string name, + object? value + ) + { + var p = command.CreateParameter(); + p.ParameterName = name; + p.Value = value ?? DBNull.Value; + command.Parameters.Add(p); + } +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs new file mode 100644 index 0000000..619276d --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs @@ -0,0 +1,67 @@ +using Domain.Core.Entities; + +namespace Infrastructure.Repository.Auth; + +/// +/// Repository for authentication-related database operations including user registration and credential management. +/// +public interface IAuthRepository +{ + /// + /// Registers a new user with account details and initial credential. + /// Uses stored procedure: USP_RegisterUser + /// + /// Unique username for the user + /// User's first name + /// User's last name + /// User's email address + /// User's date of birth + /// Hashed password + /// The newly created UserAccount with generated ID + Task RegisterUserAsync( + string username, + string firstName, + string lastName, + string email, + DateTime dateOfBirth, + string passwordHash + ); + + /// + /// Retrieves a user account by email address (typically used for login). + /// Uses stored procedure: usp_GetUserAccountByEmail + /// + /// Email address to search for + /// UserAccount if found, null otherwise + Task GetUserByEmailAsync( + string email + ); + + /// + /// Retrieves a user account by username (typically used for login). + /// Uses stored procedure: usp_GetUserAccountByUsername + /// + /// Username to search for + /// UserAccount if found, null otherwise + Task GetUserByUsernameAsync( + string username + ); + + /// + /// Retrieves the active (non-revoked) credential for a user account. + /// Uses stored procedure: USP_GetActiveUserCredentialByUserAccountId + /// + /// ID of the user account + /// Active UserCredential if found, null otherwise + Task GetActiveCredentialByUserAccountIdAsync( + Guid userAccountId + ); + + /// + /// Rotates a user's credential by invalidating all existing credentials and creating a new one. + /// Uses stored procedure: USP_RotateUserCredential + /// + /// ID of the user account + /// New hashed password + Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash); +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj b/src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj similarity index 83% rename from src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj rename to src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj index 9268fc3..178bda8 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj +++ b/src/Core/Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj @@ -3,7 +3,7 @@ net10.0 enable enable - Repository.Core + Infrastructure.Repository @@ -18,6 +18,6 @@ /> - + diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs deleted file mode 100644 index 3b7c536..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/AuthRepository.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System.Data; -using System.Data.Common; -using Domain.Core.Entities; -using Repository.Core.Sql; - -namespace Repository.Core.Repositories.Auth -{ - public class AuthRepository - : Repository, - IAuthRepository - { - public AuthRepository(ISqlConnectionFactory connectionFactory) - : base(connectionFactory) { } - - public async Task RegisterUserAsync( - string username, - string firstName, - string lastName, - string email, - DateTime dateOfBirth, - string passwordHash - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - - command.CommandText = "USP_RegisterUser"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@Username", username); - AddParameter(command, "@FirstName", firstName); - AddParameter(command, "@LastName", lastName); - AddParameter(command, "@Email", email); - AddParameter(command, "@DateOfBirth", dateOfBirth); - AddParameter(command, "@Hash", passwordHash); - - var result = await command.ExecuteScalarAsync(); - var userAccountId = result != null ? (Guid)result : Guid.Empty; - - return new Domain.Core.Entities.UserAccount - { - UserAccountId = userAccountId, - Username = username, - FirstName = firstName, - LastName = lastName, - Email = email, - DateOfBirth = dateOfBirth, - CreatedAt = DateTime.UtcNow, - }; - } - - public async Task GetUserByEmailAsync( - string email - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_GetUserAccountByEmail"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@Email", email); - - await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() ? MapToEntity(reader) : null; - } - - public async Task GetUserByUsernameAsync( - string username - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_GetUserAccountByUsername"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@Username", username); - - await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() ? MapToEntity(reader) : null; - } - - public async Task GetActiveCredentialByUserAccountIdAsync( - Guid userAccountId - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "USP_GetActiveUserCredentialByUserAccountId"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@UserAccountId", userAccountId); - - await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() - ? MapToCredentialEntity(reader) - : null; - } - - public async Task RotateCredentialAsync( - Guid userAccountId, - string newPasswordHash - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "USP_RotateUserCredential"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@UserAccountId_", userAccountId); - AddParameter(command, "@Hash", newPasswordHash); - - await command.ExecuteNonQueryAsync(); - } - - /// - /// Maps a data reader row to a UserAccount entity. - /// - protected override Domain.Core.Entities.UserAccount MapToEntity( - DbDataReader reader - ) - { - return new Domain.Core.Entities.UserAccount - { - UserAccountId = reader.GetGuid( - reader.GetOrdinal("UserAccountId") - ), - Username = reader.GetString(reader.GetOrdinal("Username")), - FirstName = reader.GetString(reader.GetOrdinal("FirstName")), - LastName = reader.GetString(reader.GetOrdinal("LastName")), - Email = reader.GetString(reader.GetOrdinal("Email")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), - UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) - ? null - : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), - DateOfBirth = reader.GetDateTime( - reader.GetOrdinal("DateOfBirth") - ), - Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) - ? null - : (byte[])reader["Timer"], - }; - } - - /// - /// Maps a data reader row to a UserCredential entity. - /// - private static UserCredential MapToCredentialEntity(DbDataReader reader) - { - var entity = new UserCredential - { - UserCredentialId = reader.GetGuid( - reader.GetOrdinal("UserCredentialId") - ), - UserAccountId = reader.GetGuid( - reader.GetOrdinal("UserAccountId") - ), - Hash = reader.GetString(reader.GetOrdinal("Hash")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), - }; - - // Optional columns - var hasTimer = - reader - .GetSchemaTable() - ?.Rows.Cast() - .Any(r => - string.Equals( - r["ColumnName"]?.ToString(), - "Timer", - StringComparison.OrdinalIgnoreCase - ) - ) ?? false; - - if (hasTimer) - { - entity.Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) - ? null - : (byte[])reader["Timer"]; - } - - return entity; - } - - /// - /// Helper method to add a parameter to a database command. - /// - private static void AddParameter( - DbCommand command, - string name, - object? value - ) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs deleted file mode 100644 index 2ebd49d..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Auth/IAuthRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Domain.Core.Entities; - -namespace Repository.Core.Repositories.Auth -{ - /// - /// Repository for authentication-related database operations including user registration and credential management. - /// - public interface IAuthRepository - { - /// - /// Registers a new user with account details and initial credential. - /// Uses stored procedure: USP_RegisterUser - /// - /// Unique username for the user - /// User's first name - /// User's last name - /// User's email address - /// User's date of birth - /// Hashed password - /// The newly created UserAccount with generated ID - Task RegisterUserAsync( - string username, - string firstName, - string lastName, - string email, - DateTime dateOfBirth, - string passwordHash - ); - - /// - /// Retrieves a user account by email address (typically used for login). - /// Uses stored procedure: usp_GetUserAccountByEmail - /// - /// Email address to search for - /// UserAccount if found, null otherwise - Task GetUserByEmailAsync( - string email - ); - - /// - /// Retrieves a user account by username (typically used for login). - /// Uses stored procedure: usp_GetUserAccountByUsername - /// - /// Username to search for - /// UserAccount if found, null otherwise - Task GetUserByUsernameAsync( - string username - ); - - /// - /// Retrieves the active (non-revoked) credential for a user account. - /// Uses stored procedure: USP_GetActiveUserCredentialByUserAccountId - /// - /// ID of the user account - /// Active UserCredential if found, null otherwise - Task GetActiveCredentialByUserAccountIdAsync( - Guid userAccountId - ); - - /// - /// Rotates a user's credential by invalidating all existing credentials and creating a new one. - /// Uses stored procedure: USP_RotateUserCredential - /// - /// ID of the user account - /// New hashed password - Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash); - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Repository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Repository.cs deleted file mode 100644 index 6ac9eab..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/Repository.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Data.Common; -using Repository.Core.Sql; - -namespace Repository.Core.Repositories -{ - public abstract class Repository(ISqlConnectionFactory connectionFactory) - where T : class - { - protected async Task CreateConnection() - { - var connection = connectionFactory.CreateConnection(); - await connection.OpenAsync(); - return connection; - } - - protected abstract T MapToEntity(DbDataReader reader); - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs deleted file mode 100644 index d578bb3..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/IUserAccountRepository.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Domain.Core.Entities; - -namespace Repository.Core.Repositories.UserAccount -{ - public interface IUserAccountRepository - { - Task GetByIdAsync(Guid id); - Task> GetAllAsync( - int? limit, - int? offset - ); - Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount); - Task DeleteAsync(Guid id); - Task GetByUsernameAsync( - string username - ); - Task GetByEmailAsync(string email); - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs deleted file mode 100644 index 7b4eb19..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Repositories/UserAccount/UserAccountRepository.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Data; -using System.Data.Common; -using Domain.Core.Entities; -using Repository.Core.Sql; - -namespace Repository.Core.Repositories.UserAccount -{ - public class UserAccountRepository(ISqlConnectionFactory connectionFactory) - : Repository(connectionFactory), - IUserAccountRepository - { - public async Task GetByIdAsync( - Guid id - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_GetUserAccountById"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@UserAccountId", id); - - await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() ? MapToEntity(reader) : null; - } - - public async Task< - IEnumerable - > GetAllAsync(int? limit, int? offset) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_GetAllUserAccounts"; - command.CommandType = CommandType.StoredProcedure; - - if (limit.HasValue) - AddParameter(command, "@Limit", limit.Value); - - if (offset.HasValue) - AddParameter(command, "@Offset", offset.Value); - - await using var reader = await command.ExecuteReaderAsync(); - var users = new List(); - - while (await reader.ReadAsync()) - { - users.Add(MapToEntity(reader)); - } - - return users; - } - - public async Task UpdateAsync( - Domain.Core.Entities.UserAccount userAccount - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_UpdateUserAccount"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@UserAccountId", userAccount.UserAccountId); - AddParameter(command, "@Username", userAccount.Username); - AddParameter(command, "@FirstName", userAccount.FirstName); - AddParameter(command, "@LastName", userAccount.LastName); - AddParameter(command, "@Email", userAccount.Email); - AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth); - - await command.ExecuteNonQueryAsync(); - } - - public async Task DeleteAsync(Guid id) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_DeleteUserAccount"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@UserAccountId", id); - await command.ExecuteNonQueryAsync(); - } - - public async Task GetByUsernameAsync( - string username - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_GetUserAccountByUsername"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@Username", username); - - await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() ? MapToEntity(reader) : null; - } - - public async Task GetByEmailAsync( - string email - ) - { - await using var connection = await CreateConnection(); - await using var command = connection.CreateCommand(); - command.CommandText = "usp_GetUserAccountByEmail"; - command.CommandType = CommandType.StoredProcedure; - - AddParameter(command, "@Email", email); - - await using var reader = await command.ExecuteReaderAsync(); - return await reader.ReadAsync() ? MapToEntity(reader) : null; - } - - protected override Domain.Core.Entities.UserAccount MapToEntity( - DbDataReader reader - ) - { - return new Domain.Core.Entities.UserAccount - { - UserAccountId = reader.GetGuid( - reader.GetOrdinal("UserAccountId") - ), - Username = reader.GetString(reader.GetOrdinal("Username")), - FirstName = reader.GetString(reader.GetOrdinal("FirstName")), - LastName = reader.GetString(reader.GetOrdinal("LastName")), - Email = reader.GetString(reader.GetOrdinal("Email")), - CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), - UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) - ? null - : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), - DateOfBirth = reader.GetDateTime( - reader.GetOrdinal("DateOfBirth") - ), - Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) - ? null - : (byte[])reader["Timer"], - }; - } - - private static void AddParameter( - DbCommand command, - string name, - object? value - ) - { - var p = command.CreateParameter(); - p.ParameterName = name; - p.Value = value ?? DBNull.Value; - command.Parameters.Add(p); - } - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs deleted file mode 100644 index e12e1b1..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Data.Common; -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Configuration; - -namespace Repository.Core.Sql -{ - public class DefaultSqlConnectionFactory(IConfiguration configuration) - : ISqlConnectionFactory - { - 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() - { - return new SqlConnection(_connectionString); - } - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/ISqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/ISqlConnectionFactory.cs deleted file mode 100644 index c8be898..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/ISqlConnectionFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Data.Common; - -namespace Repository.Core.Sql -{ - public interface ISqlConnectionFactory - { - DbConnection CreateConnection(); - } -} diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs deleted file mode 100644 index 42a991c..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Core/Sql/SqlConnectionStringHelper.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Microsoft.Data.SqlClient; - -namespace Repository.Core.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/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile b/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile deleted file mode 100644 index be8f45c..0000000 --- a/src/Core/Infrastructure/Infrastructure.Repository/Repository.Tests/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["Domain/Domain.csproj", "Domain/"] -COPY ["Infrastructure/Infrastructure.Repository/Repository.Core/Repository.Core.csproj", "Infrastructure/Infrastructure.Repository/Repository.Core/"] -COPY ["Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj", "Infrastructure/Infrastructure.Repository/Repository.Tests/"] -RUN dotnet restore "Infrastructure/Infrastructure.Repository/Repository.Tests/Repository.Tests.csproj" -COPY . . -WORKDIR "/src/Infrastructure/Infrastructure.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/Infrastructure/Infrastructure.Repository/Repository.Tests -ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"] diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Repository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Repository.cs new file mode 100644 index 0000000..30672d9 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Repository.cs @@ -0,0 +1,17 @@ +using System.Data.Common; +using Infrastructure.Repository.Sql; + +namespace Infrastructure.Repository; + +public abstract class Repository(ISqlConnectionFactory connectionFactory) + where T : class +{ + protected async Task CreateConnection() + { + var connection = connectionFactory.CreateConnection(); + await connection.OpenAsync(); + return connection; + } + + protected abstract T MapToEntity(DbDataReader reader); +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs new file mode 100644 index 0000000..8d5bf62 --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Sql/DefaultSqlConnectionFactory.cs @@ -0,0 +1,49 @@ +using System.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; + +namespace Infrastructure.Repository.Sql; + +public class DefaultSqlConnectionFactory(IConfiguration configuration) + : ISqlConnectionFactory +{ + 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() + { + return new SqlConnection(_connectionString); + } +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs b/src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs new file mode 100644 index 0000000..40a6eed --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Sql/ISqlConnectionFactory.cs @@ -0,0 +1,8 @@ +using System.Data.Common; + +namespace Infrastructure.Repository.Sql; + +public interface ISqlConnectionFactory +{ + DbConnection CreateConnection(); +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs b/src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs new file mode 100644 index 0000000..27daeff --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/Sql/SqlConnectionStringHelper.cs @@ -0,0 +1,61 @@ +using Microsoft.Data.SqlClient; + +namespace Infrastructure.Repository.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"); + } +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs new file mode 100644 index 0000000..e1929ca --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs @@ -0,0 +1,16 @@ +namespace Infrastructure.Repository.UserAccount; + +public interface IUserAccountRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync( + int? limit, + int? offset + ); + Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount); + Task DeleteAsync(Guid id); + Task GetByUsernameAsync( + string username + ); + Task GetByEmailAsync(string email); +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs new file mode 100644 index 0000000..6d9252b --- /dev/null +++ b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs @@ -0,0 +1,149 @@ +using System.Data; +using System.Data.Common; +using Infrastructure.Repository.Sql; + +namespace Infrastructure.Repository.UserAccount; + +public class UserAccountRepository(ISqlConnectionFactory connectionFactory) + : Repository(connectionFactory), + IUserAccountRepository +{ + public async Task GetByIdAsync( + Guid id + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_GetUserAccountById"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@UserAccountId", id); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + public async Task< + IEnumerable + > GetAllAsync(int? limit, int? offset) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_GetAllUserAccounts"; + command.CommandType = CommandType.StoredProcedure; + + if (limit.HasValue) + AddParameter(command, "@Limit", limit.Value); + + if (offset.HasValue) + AddParameter(command, "@Offset", offset.Value); + + await using var reader = await command.ExecuteReaderAsync(); + var users = new List(); + + while (await reader.ReadAsync()) + { + users.Add(MapToEntity(reader)); + } + + return users; + } + + public async Task UpdateAsync( + Domain.Core.Entities.UserAccount userAccount + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_UpdateUserAccount"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@UserAccountId", userAccount.UserAccountId); + AddParameter(command, "@Username", userAccount.Username); + AddParameter(command, "@FirstName", userAccount.FirstName); + AddParameter(command, "@LastName", userAccount.LastName); + AddParameter(command, "@Email", userAccount.Email); + AddParameter(command, "@DateOfBirth", userAccount.DateOfBirth); + + await command.ExecuteNonQueryAsync(); + } + + public async Task DeleteAsync(Guid id) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_DeleteUserAccount"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@UserAccountId", id); + await command.ExecuteNonQueryAsync(); + } + + public async Task GetByUsernameAsync( + string username + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_GetUserAccountByUsername"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@Username", username); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + public async Task GetByEmailAsync( + string email + ) + { + await using var connection = await CreateConnection(); + await using var command = connection.CreateCommand(); + command.CommandText = "usp_GetUserAccountByEmail"; + command.CommandType = CommandType.StoredProcedure; + + AddParameter(command, "@Email", email); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? MapToEntity(reader) : null; + } + + protected override Domain.Core.Entities.UserAccount MapToEntity( + DbDataReader reader + ) + { + return new Domain.Core.Entities.UserAccount + { + UserAccountId = reader.GetGuid( + reader.GetOrdinal("UserAccountId") + ), + Username = reader.GetString(reader.GetOrdinal("Username")), + FirstName = reader.GetString(reader.GetOrdinal("FirstName")), + LastName = reader.GetString(reader.GetOrdinal("LastName")), + Email = reader.GetString(reader.GetOrdinal("Email")), + CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), + UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt")) + ? null + : reader.GetDateTime(reader.GetOrdinal("UpdatedAt")), + DateOfBirth = reader.GetDateTime( + reader.GetOrdinal("DateOfBirth") + ), + Timer = reader.IsDBNull(reader.GetOrdinal("Timer")) + ? null + : (byte[])reader["Timer"], + }; + } + + private static void AddParameter( + DbCommand command, + string name, + object? value + ) + { + var p = command.CreateParameter(); + p.ParameterName = name; + p.Value = value ?? DBNull.Value; + command.Parameters.Add(p); + } +} \ No newline at end of file diff --git a/src/Core/Service/Service.Core/Auth/AuthService.cs b/src/Core/Service/Service.Core/Auth/AuthService.cs index 51dbbc8..738a0ae 100644 --- a/src/Core/Service/Service.Core/Auth/AuthService.cs +++ b/src/Core/Service/Service.Core/Auth/AuthService.cs @@ -1,6 +1,6 @@ using Domain.Core.Entities; using Infrastructure.PasswordHashing; -using Repository.Core.Repositories.Auth; +using Infrastructure.Repository.Auth; namespace Service.Core.Auth; diff --git a/src/Core/Service/Service.Core/Service.Core.csproj b/src/Core/Service/Service.Core/Service.Core.csproj index 595fd7d..c01d95b 100644 --- a/src/Core/Service/Service.Core/Service.Core.csproj +++ b/src/Core/Service/Service.Core/Service.Core.csproj @@ -12,8 +12,7 @@ - + diff --git a/src/Core/Service/Service.Core/User/UserService.cs b/src/Core/Service/Service.Core/User/UserService.cs index fb249cd..977c94c 100644 --- a/src/Core/Service/Service.Core/User/UserService.cs +++ b/src/Core/Service/Service.Core/User/UserService.cs @@ -1,5 +1,5 @@ using Domain.Core.Entities; -using Repository.Core.Repositories.UserAccount; +using Infrastructure.Repository.UserAccount; namespace Service.Core.User; From f728514a7c659e4b5733bee8a2317b8aab6735be Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 17:50:08 -0500 Subject: [PATCH 6/7] Update namespaces --- .../API.Core/Controllers/AuthController.cs | 2 +- .../API.Core/Controllers/UserController.cs | 2 +- src/Core/Core.slnx | 2 +- src/Core/Database/Database.Seed/ISeeder.cs | 5 +- .../Database/Database.Seed/LocationSeeder.cs | 616 +++++++++--------- src/Core/Database/Database.Seed/Program.cs | 18 +- src/Core/Database/Database.Seed/UserSeeder.cs | 505 +++++++------- src/Core/Domain/Entities/UserAccount.cs | 2 +- src/Core/Domain/Entities/UserCredential.cs | 2 +- src/Core/Domain/Entities/UserVerification.cs | 2 +- ...=> Infrastructure.Repository.Tests.csproj} | 0 .../Auth/AuthRepository.cs | 21 +- .../Auth/IAuthRepository.cs | 8 +- .../UserAccount/IUserAccountRepository.cs | 10 +- .../UserAccount/UserAccountRepository.cs | 18 +- .../Service/Service.Core/Auth/AuthService.cs | 2 +- .../Service/Service.Core/Auth/IAuthService.cs | 2 +- .../Service/Service.Core/User/IUserService.cs | 2 +- .../Service/Service.Core/User/UserService.cs | 2 +- 19 files changed, 604 insertions(+), 617 deletions(-) rename src/Core/Infrastructure/Infrastructure.Repository.Tests/{Repository.Tests.csproj => Infrastructure.Repository.Tests.csproj} (100%) diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs index 91440ff..62a9018 100644 --- a/src/Core/API/API.Core/Controllers/AuthController.cs +++ b/src/Core/API/API.Core/Controllers/AuthController.cs @@ -1,6 +1,6 @@ using API.Core.Contracts.Auth; using API.Core.Contracts.Common; -using Domain.Core.Entities; +using Domain.Entities; using Infrastructure.Jwt; using Microsoft.AspNetCore.Mvc; using Service.Core.Auth; diff --git a/src/Core/API/API.Core/Controllers/UserController.cs b/src/Core/API/API.Core/Controllers/UserController.cs index 5077e44..6039ce6 100644 --- a/src/Core/API/API.Core/Controllers/UserController.cs +++ b/src/Core/API/API.Core/Controllers/UserController.cs @@ -1,4 +1,4 @@ -using Domain.Core.Entities; +using Domain.Entities; using Microsoft.AspNetCore.Mvc; using Service.Core.User; diff --git a/src/Core/Core.slnx b/src/Core/Core.slnx index 293b22b..a9f0a9f 100644 --- a/src/Core/Core.slnx +++ b/src/Core/Core.slnx @@ -13,8 +13,8 @@ - + diff --git a/src/Core/Database/Database.Seed/ISeeder.cs b/src/Core/Database/Database.Seed/ISeeder.cs index 1ecf897..b3a414e 100644 --- a/src/Core/Database/Database.Seed/ISeeder.cs +++ b/src/Core/Database/Database.Seed/ISeeder.cs @@ -2,8 +2,5 @@ using Microsoft.Data.SqlClient; namespace DBSeed { - internal interface ISeeder - { - Task SeedAsync(SqlConnection connection); - } + Task SeedAsync(SqlConnection connection); } \ No newline at end of file diff --git a/src/Core/Database/Database.Seed/LocationSeeder.cs b/src/Core/Database/Database.Seed/LocationSeeder.cs index 52d3d78..9e42050 100644 --- a/src/Core/Database/Database.Seed/LocationSeeder.cs +++ b/src/Core/Database/Database.Seed/LocationSeeder.cs @@ -1,328 +1,326 @@ using System.Data; using Microsoft.Data.SqlClient; -namespace DBSeed +namespace Database.Seed; + +internal class LocationSeeder : ISeeder { + private static readonly IReadOnlyList<( + string CountryName, + string CountryCode + )> Countries = + [ + ("Canada", "CA"), + ("Mexico", "MX"), + ("United States", "US"), + ]; - internal class LocationSeeder : ISeeder + private static IReadOnlyList<(string StateProvinceName, string StateProvinceCode, string CountryCode)> States { - private static readonly IReadOnlyList<( - string CountryName, - string CountryCode - )> Countries = - [ - ("Canada", "CA"), - ("Mexico", "MX"), - ("United States", "US"), - ]; + get; + } = + [ + ("Alabama", "US-AL", "US"), + ("Alaska", "US-AK", "US"), + ("Arizona", "US-AZ", "US"), + ("Arkansas", "US-AR", "US"), + ("California", "US-CA", "US"), + ("Colorado", "US-CO", "US"), + ("Connecticut", "US-CT", "US"), + ("Delaware", "US-DE", "US"), + ("Florida", "US-FL", "US"), + ("Georgia", "US-GA", "US"), + ("Hawaii", "US-HI", "US"), + ("Idaho", "US-ID", "US"), + ("Illinois", "US-IL", "US"), + ("Indiana", "US-IN", "US"), + ("Iowa", "US-IA", "US"), + ("Kansas", "US-KS", "US"), + ("Kentucky", "US-KY", "US"), + ("Louisiana", "US-LA", "US"), + ("Maine", "US-ME", "US"), + ("Maryland", "US-MD", "US"), + ("Massachusetts", "US-MA", "US"), + ("Michigan", "US-MI", "US"), + ("Minnesota", "US-MN", "US"), + ("Mississippi", "US-MS", "US"), + ("Missouri", "US-MO", "US"), + ("Montana", "US-MT", "US"), + ("Nebraska", "US-NE", "US"), + ("Nevada", "US-NV", "US"), + ("New Hampshire", "US-NH", "US"), + ("New Jersey", "US-NJ", "US"), + ("New Mexico", "US-NM", "US"), + ("New York", "US-NY", "US"), + ("North Carolina", "US-NC", "US"), + ("North Dakota", "US-ND", "US"), + ("Ohio", "US-OH", "US"), + ("Oklahoma", "US-OK", "US"), + ("Oregon", "US-OR", "US"), + ("Pennsylvania", "US-PA", "US"), + ("Rhode Island", "US-RI", "US"), + ("South Carolina", "US-SC", "US"), + ("South Dakota", "US-SD", "US"), + ("Tennessee", "US-TN", "US"), + ("Texas", "US-TX", "US"), + ("Utah", "US-UT", "US"), + ("Vermont", "US-VT", "US"), + ("Virginia", "US-VA", "US"), + ("Washington", "US-WA", "US"), + ("West Virginia", "US-WV", "US"), + ("Wisconsin", "US-WI", "US"), + ("Wyoming", "US-WY", "US"), + ("District of Columbia", "US-DC", "US"), + ("Puerto Rico", "US-PR", "US"), + ("U.S. Virgin Islands", "US-VI", "US"), + ("Guam", "US-GU", "US"), + ("Northern Mariana Islands", "US-MP", "US"), + ("American Samoa", "US-AS", "US"), + ("Ontario", "CA-ON", "CA"), + ("Québec", "CA-QC", "CA"), + ("Nova Scotia", "CA-NS", "CA"), + ("New Brunswick", "CA-NB", "CA"), + ("Manitoba", "CA-MB", "CA"), + ("British Columbia", "CA-BC", "CA"), + ("Prince Edward Island", "CA-PE", "CA"), + ("Saskatchewan", "CA-SK", "CA"), + ("Alberta", "CA-AB", "CA"), + ("Newfoundland and Labrador", "CA-NL", "CA"), + ("Northwest Territories", "CA-NT", "CA"), + ("Yukon", "CA-YT", "CA"), + ("Nunavut", "CA-NU", "CA"), + ("Aguascalientes", "MX-AGU", "MX"), + ("Baja California", "MX-BCN", "MX"), + ("Baja California Sur", "MX-BCS", "MX"), + ("Campeche", "MX-CAM", "MX"), + ("Chiapas", "MX-CHP", "MX"), + ("Chihuahua", "MX-CHH", "MX"), + ("Coahuila de Zaragoza", "MX-COA", "MX"), + ("Colima", "MX-COL", "MX"), + ("Durango", "MX-DUR", "MX"), + ("Guanajuato", "MX-GUA", "MX"), + ("Guerrero", "MX-GRO", "MX"), + ("Hidalgo", "MX-HID", "MX"), + ("Jalisco", "MX-JAL", "MX"), + ("México State", "MX-MEX", "MX"), + ("Michoacán de Ocampo", "MX-MIC", "MX"), + ("Morelos", "MX-MOR", "MX"), + ("Nayarit", "MX-NAY", "MX"), + ("Nuevo León", "MX-NLE", "MX"), + ("Oaxaca", "MX-OAX", "MX"), + ("Puebla", "MX-PUE", "MX"), + ("Querétaro", "MX-QUE", "MX"), + ("Quintana Roo", "MX-ROO", "MX"), + ("San Luis Potosí", "MX-SLP", "MX"), + ("Sinaloa", "MX-SIN", "MX"), + ("Sonora", "MX-SON", "MX"), + ("Tabasco", "MX-TAB", "MX"), + ("Tamaulipas", "MX-TAM", "MX"), + ("Tlaxcala", "MX-TLA", "MX"), + ("Veracruz de Ignacio de la Llave", "MX-VER", "MX"), + ("Yucatán", "MX-YUC", "MX"), + ("Zacatecas", "MX-ZAC", "MX"), + ("Ciudad de México", "MX-CMX", "MX"), + ]; - private static IReadOnlyList<(string StateProvinceName, string StateProvinceCode, string CountryCode)> States + private static IReadOnlyList<(string StateProvinceCode, string CityName)> Cities { get; } = + [ + ("US-CA", "Los Angeles"), + ("US-CA", "San Diego"), + ("US-CA", "San Francisco"), + ("US-CA", "Sacramento"), + ("US-TX", "Houston"), + ("US-TX", "Dallas"), + ("US-TX", "Austin"), + ("US-TX", "San Antonio"), + ("US-FL", "Miami"), + ("US-FL", "Orlando"), + ("US-FL", "Tampa"), + ("US-NY", "New York"), + ("US-NY", "Buffalo"), + ("US-NY", "Rochester"), + ("US-IL", "Chicago"), + ("US-IL", "Springfield"), + ("US-PA", "Philadelphia"), + ("US-PA", "Pittsburgh"), + ("US-AZ", "Phoenix"), + ("US-AZ", "Tucson"), + ("US-CO", "Denver"), + ("US-CO", "Colorado Springs"), + ("US-MA", "Boston"), + ("US-MA", "Worcester"), + ("US-WA", "Seattle"), + ("US-WA", "Spokane"), + ("US-GA", "Atlanta"), + ("US-GA", "Savannah"), + ("US-NV", "Las Vegas"), + ("US-NV", "Reno"), + ("US-MI", "Detroit"), + ("US-MI", "Grand Rapids"), + ("US-MN", "Minneapolis"), + ("US-MN", "Saint Paul"), + ("US-OH", "Columbus"), + ("US-OH", "Cleveland"), + ("US-OR", "Portland"), + ("US-OR", "Salem"), + ("US-TN", "Nashville"), + ("US-TN", "Memphis"), + ("US-VA", "Richmond"), + ("US-VA", "Virginia Beach"), + ("US-MD", "Baltimore"), + ("US-MD", "Frederick"), + ("US-DC", "Washington"), + ("US-UT", "Salt Lake City"), + ("US-UT", "Provo"), + ("US-LA", "New Orleans"), + ("US-LA", "Baton Rouge"), + ("US-KY", "Louisville"), + ("US-KY", "Lexington"), + ("US-IA", "Des Moines"), + ("US-IA", "Cedar Rapids"), + ("US-OK", "Oklahoma City"), + ("US-OK", "Tulsa"), + ("US-NE", "Omaha"), + ("US-NE", "Lincoln"), + ("US-MO", "Kansas City"), + ("US-MO", "St. Louis"), + ("US-NC", "Charlotte"), + ("US-NC", "Raleigh"), + ("US-SC", "Columbia"), + ("US-SC", "Charleston"), + ("US-WI", "Milwaukee"), + ("US-WI", "Madison"), + ("US-MN", "Duluth"), + ("US-AK", "Anchorage"), + ("US-HI", "Honolulu"), + ("CA-ON", "Toronto"), + ("CA-ON", "Ottawa"), + ("CA-QC", "Montréal"), + ("CA-QC", "Québec City"), + ("CA-BC", "Vancouver"), + ("CA-BC", "Victoria"), + ("CA-AB", "Calgary"), + ("CA-AB", "Edmonton"), + ("CA-MB", "Winnipeg"), + ("CA-NS", "Halifax"), + ("CA-SK", "Saskatoon"), + ("CA-SK", "Regina"), + ("CA-NB", "Moncton"), + ("CA-NB", "Saint John"), + ("CA-PE", "Charlottetown"), + ("CA-NL", "St. John's"), + ("CA-ON", "Hamilton"), + ("CA-ON", "London"), + ("CA-QC", "Gatineau"), + ("CA-QC", "Laval"), + ("CA-BC", "Kelowna"), + ("CA-AB", "Red Deer"), + ("CA-MB", "Brandon"), + ("MX-CMX", "Ciudad de México"), + ("MX-JAL", "Guadalajara"), + ("MX-NLE", "Monterrey"), + ("MX-PUE", "Puebla"), + ("MX-ROO", "Cancún"), + ("MX-GUA", "Guanajuato"), + ("MX-MIC", "Morelia"), + ("MX-BCN", "Tijuana"), + ("MX-JAL", "Zapopan"), + ("MX-NLE", "San Nicolás"), + ("MX-CAM", "Campeche"), + ("MX-TAB", "Villahermosa"), + ("MX-VER", "Veracruz"), + ("MX-OAX", "Oaxaca"), + ("MX-SLP", "San Luis Potosí"), + ("MX-CHH", "Chihuahua"), + ("MX-AGU", "Aguascalientes"), + ("MX-MEX", "Toluca"), + ("MX-COA", "Saltillo"), + ("MX-BCS", "La Paz"), + ("MX-NAY", "Tepic"), + ("MX-ZAC", "Zacatecas"), + ]; + + public async Task SeedAsync(SqlConnection connection) + { + foreach (var (countryName, countryCode) in Countries) { - get; - } = - [ - ("Alabama", "US-AL", "US"), - ("Alaska", "US-AK", "US"), - ("Arizona", "US-AZ", "US"), - ("Arkansas", "US-AR", "US"), - ("California", "US-CA", "US"), - ("Colorado", "US-CO", "US"), - ("Connecticut", "US-CT", "US"), - ("Delaware", "US-DE", "US"), - ("Florida", "US-FL", "US"), - ("Georgia", "US-GA", "US"), - ("Hawaii", "US-HI", "US"), - ("Idaho", "US-ID", "US"), - ("Illinois", "US-IL", "US"), - ("Indiana", "US-IN", "US"), - ("Iowa", "US-IA", "US"), - ("Kansas", "US-KS", "US"), - ("Kentucky", "US-KY", "US"), - ("Louisiana", "US-LA", "US"), - ("Maine", "US-ME", "US"), - ("Maryland", "US-MD", "US"), - ("Massachusetts", "US-MA", "US"), - ("Michigan", "US-MI", "US"), - ("Minnesota", "US-MN", "US"), - ("Mississippi", "US-MS", "US"), - ("Missouri", "US-MO", "US"), - ("Montana", "US-MT", "US"), - ("Nebraska", "US-NE", "US"), - ("Nevada", "US-NV", "US"), - ("New Hampshire", "US-NH", "US"), - ("New Jersey", "US-NJ", "US"), - ("New Mexico", "US-NM", "US"), - ("New York", "US-NY", "US"), - ("North Carolina", "US-NC", "US"), - ("North Dakota", "US-ND", "US"), - ("Ohio", "US-OH", "US"), - ("Oklahoma", "US-OK", "US"), - ("Oregon", "US-OR", "US"), - ("Pennsylvania", "US-PA", "US"), - ("Rhode Island", "US-RI", "US"), - ("South Carolina", "US-SC", "US"), - ("South Dakota", "US-SD", "US"), - ("Tennessee", "US-TN", "US"), - ("Texas", "US-TX", "US"), - ("Utah", "US-UT", "US"), - ("Vermont", "US-VT", "US"), - ("Virginia", "US-VA", "US"), - ("Washington", "US-WA", "US"), - ("West Virginia", "US-WV", "US"), - ("Wisconsin", "US-WI", "US"), - ("Wyoming", "US-WY", "US"), - ("District of Columbia", "US-DC", "US"), - ("Puerto Rico", "US-PR", "US"), - ("U.S. Virgin Islands", "US-VI", "US"), - ("Guam", "US-GU", "US"), - ("Northern Mariana Islands", "US-MP", "US"), - ("American Samoa", "US-AS", "US"), - ("Ontario", "CA-ON", "CA"), - ("Québec", "CA-QC", "CA"), - ("Nova Scotia", "CA-NS", "CA"), - ("New Brunswick", "CA-NB", "CA"), - ("Manitoba", "CA-MB", "CA"), - ("British Columbia", "CA-BC", "CA"), - ("Prince Edward Island", "CA-PE", "CA"), - ("Saskatchewan", "CA-SK", "CA"), - ("Alberta", "CA-AB", "CA"), - ("Newfoundland and Labrador", "CA-NL", "CA"), - ("Northwest Territories", "CA-NT", "CA"), - ("Yukon", "CA-YT", "CA"), - ("Nunavut", "CA-NU", "CA"), - ("Aguascalientes", "MX-AGU", "MX"), - ("Baja California", "MX-BCN", "MX"), - ("Baja California Sur", "MX-BCS", "MX"), - ("Campeche", "MX-CAM", "MX"), - ("Chiapas", "MX-CHP", "MX"), - ("Chihuahua", "MX-CHH", "MX"), - ("Coahuila de Zaragoza", "MX-COA", "MX"), - ("Colima", "MX-COL", "MX"), - ("Durango", "MX-DUR", "MX"), - ("Guanajuato", "MX-GUA", "MX"), - ("Guerrero", "MX-GRO", "MX"), - ("Hidalgo", "MX-HID", "MX"), - ("Jalisco", "MX-JAL", "MX"), - ("México State", "MX-MEX", "MX"), - ("Michoacán de Ocampo", "MX-MIC", "MX"), - ("Morelos", "MX-MOR", "MX"), - ("Nayarit", "MX-NAY", "MX"), - ("Nuevo León", "MX-NLE", "MX"), - ("Oaxaca", "MX-OAX", "MX"), - ("Puebla", "MX-PUE", "MX"), - ("Querétaro", "MX-QUE", "MX"), - ("Quintana Roo", "MX-ROO", "MX"), - ("San Luis Potosí", "MX-SLP", "MX"), - ("Sinaloa", "MX-SIN", "MX"), - ("Sonora", "MX-SON", "MX"), - ("Tabasco", "MX-TAB", "MX"), - ("Tamaulipas", "MX-TAM", "MX"), - ("Tlaxcala", "MX-TLA", "MX"), - ("Veracruz de Ignacio de la Llave", "MX-VER", "MX"), - ("Yucatán", "MX-YUC", "MX"), - ("Zacatecas", "MX-ZAC", "MX"), - ("Ciudad de México", "MX-CMX", "MX"), - ]; - - private static IReadOnlyList<(string StateProvinceCode, string CityName)> Cities { get; } = - [ - ("US-CA", "Los Angeles"), - ("US-CA", "San Diego"), - ("US-CA", "San Francisco"), - ("US-CA", "Sacramento"), - ("US-TX", "Houston"), - ("US-TX", "Dallas"), - ("US-TX", "Austin"), - ("US-TX", "San Antonio"), - ("US-FL", "Miami"), - ("US-FL", "Orlando"), - ("US-FL", "Tampa"), - ("US-NY", "New York"), - ("US-NY", "Buffalo"), - ("US-NY", "Rochester"), - ("US-IL", "Chicago"), - ("US-IL", "Springfield"), - ("US-PA", "Philadelphia"), - ("US-PA", "Pittsburgh"), - ("US-AZ", "Phoenix"), - ("US-AZ", "Tucson"), - ("US-CO", "Denver"), - ("US-CO", "Colorado Springs"), - ("US-MA", "Boston"), - ("US-MA", "Worcester"), - ("US-WA", "Seattle"), - ("US-WA", "Spokane"), - ("US-GA", "Atlanta"), - ("US-GA", "Savannah"), - ("US-NV", "Las Vegas"), - ("US-NV", "Reno"), - ("US-MI", "Detroit"), - ("US-MI", "Grand Rapids"), - ("US-MN", "Minneapolis"), - ("US-MN", "Saint Paul"), - ("US-OH", "Columbus"), - ("US-OH", "Cleveland"), - ("US-OR", "Portland"), - ("US-OR", "Salem"), - ("US-TN", "Nashville"), - ("US-TN", "Memphis"), - ("US-VA", "Richmond"), - ("US-VA", "Virginia Beach"), - ("US-MD", "Baltimore"), - ("US-MD", "Frederick"), - ("US-DC", "Washington"), - ("US-UT", "Salt Lake City"), - ("US-UT", "Provo"), - ("US-LA", "New Orleans"), - ("US-LA", "Baton Rouge"), - ("US-KY", "Louisville"), - ("US-KY", "Lexington"), - ("US-IA", "Des Moines"), - ("US-IA", "Cedar Rapids"), - ("US-OK", "Oklahoma City"), - ("US-OK", "Tulsa"), - ("US-NE", "Omaha"), - ("US-NE", "Lincoln"), - ("US-MO", "Kansas City"), - ("US-MO", "St. Louis"), - ("US-NC", "Charlotte"), - ("US-NC", "Raleigh"), - ("US-SC", "Columbia"), - ("US-SC", "Charleston"), - ("US-WI", "Milwaukee"), - ("US-WI", "Madison"), - ("US-MN", "Duluth"), - ("US-AK", "Anchorage"), - ("US-HI", "Honolulu"), - ("CA-ON", "Toronto"), - ("CA-ON", "Ottawa"), - ("CA-QC", "Montréal"), - ("CA-QC", "Québec City"), - ("CA-BC", "Vancouver"), - ("CA-BC", "Victoria"), - ("CA-AB", "Calgary"), - ("CA-AB", "Edmonton"), - ("CA-MB", "Winnipeg"), - ("CA-NS", "Halifax"), - ("CA-SK", "Saskatoon"), - ("CA-SK", "Regina"), - ("CA-NB", "Moncton"), - ("CA-NB", "Saint John"), - ("CA-PE", "Charlottetown"), - ("CA-NL", "St. John's"), - ("CA-ON", "Hamilton"), - ("CA-ON", "London"), - ("CA-QC", "Gatineau"), - ("CA-QC", "Laval"), - ("CA-BC", "Kelowna"), - ("CA-AB", "Red Deer"), - ("CA-MB", "Brandon"), - ("MX-CMX", "Ciudad de México"), - ("MX-JAL", "Guadalajara"), - ("MX-NLE", "Monterrey"), - ("MX-PUE", "Puebla"), - ("MX-ROO", "Cancún"), - ("MX-GUA", "Guanajuato"), - ("MX-MIC", "Morelia"), - ("MX-BCN", "Tijuana"), - ("MX-JAL", "Zapopan"), - ("MX-NLE", "San Nicolás"), - ("MX-CAM", "Campeche"), - ("MX-TAB", "Villahermosa"), - ("MX-VER", "Veracruz"), - ("MX-OAX", "Oaxaca"), - ("MX-SLP", "San Luis Potosí"), - ("MX-CHH", "Chihuahua"), - ("MX-AGU", "Aguascalientes"), - ("MX-MEX", "Toluca"), - ("MX-COA", "Saltillo"), - ("MX-BCS", "La Paz"), - ("MX-NAY", "Tepic"), - ("MX-ZAC", "Zacatecas"), - ]; - - public async Task SeedAsync(SqlConnection connection) - { - foreach (var (countryName, countryCode) in Countries) - { - await CreateCountryAsync(connection, countryName, countryCode); - } - - foreach ( - var (stateProvinceName, stateProvinceCode, countryCode) in States - ) - { - await CreateStateProvinceAsync( - connection, - stateProvinceName, - stateProvinceCode, - countryCode - ); - } - - foreach (var (stateProvinceCode, cityName) in Cities) - { - await CreateCityAsync(connection, cityName, stateProvinceCode); - } + await CreateCountryAsync(connection, countryName, countryCode); } - private static async Task CreateCountryAsync( - SqlConnection connection, - string countryName, - string countryCode + foreach ( + var (stateProvinceName, stateProvinceCode, countryCode) in States ) { - await using var command = new SqlCommand( - "dbo.USP_CreateCountry", - connection + await CreateStateProvinceAsync( + connection, + stateProvinceName, + stateProvinceCode, + countryCode ); - command.CommandType = CommandType.StoredProcedure; - command.Parameters.AddWithValue("@CountryName", countryName); - command.Parameters.AddWithValue("@ISO3616_1", countryCode); - - await command.ExecuteNonQueryAsync(); } - private static async Task CreateStateProvinceAsync( - SqlConnection connection, - string stateProvinceName, - string stateProvinceCode, - string countryCode - ) + foreach (var (stateProvinceCode, cityName) in Cities) { - await using var command = new SqlCommand( - "dbo.USP_CreateStateProvince", - connection - ); - command.CommandType = CommandType.StoredProcedure; - command.Parameters.AddWithValue( - "@StateProvinceName", - stateProvinceName - ); - command.Parameters.AddWithValue("@ISO3616_2", stateProvinceCode); - command.Parameters.AddWithValue("@CountryCode", countryCode); - - await command.ExecuteNonQueryAsync(); - } - - private static async Task CreateCityAsync( - SqlConnection connection, - string cityName, - string stateProvinceCode - ) - { - await using var command = new SqlCommand( - "dbo.USP_CreateCity", - connection - ); - command.CommandType = CommandType.StoredProcedure; - command.Parameters.AddWithValue("@CityName", cityName); - command.Parameters.AddWithValue( - "@StateProvinceCode", - stateProvinceCode - ); - - await command.ExecuteNonQueryAsync(); + await CreateCityAsync(connection, cityName, stateProvinceCode); } } + + private static async Task CreateCountryAsync( + SqlConnection connection, + string countryName, + string countryCode + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateCountry", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@CountryName", countryName); + command.Parameters.AddWithValue("@ISO3616_1", countryCode); + + await command.ExecuteNonQueryAsync(); + } + + private static async Task CreateStateProvinceAsync( + SqlConnection connection, + string stateProvinceName, + string stateProvinceCode, + string countryCode + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateStateProvince", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue( + "@StateProvinceName", + stateProvinceName + ); + command.Parameters.AddWithValue("@ISO3616_2", stateProvinceCode); + command.Parameters.AddWithValue("@CountryCode", countryCode); + + await command.ExecuteNonQueryAsync(); + } + + private static async Task CreateCityAsync( + SqlConnection connection, + string cityName, + string stateProvinceCode + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateCity", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@CityName", cityName); + command.Parameters.AddWithValue( + "@StateProvinceCode", + stateProvinceCode + ); + + await command.ExecuteNonQueryAsync(); + } } \ No newline at end of file diff --git a/src/Core/Database/Database.Seed/Program.cs b/src/Core/Database/Database.Seed/Program.cs index e7fff96..c7d064d 100644 --- a/src/Core/Database/Database.Seed/Program.cs +++ b/src/Core/Database/Database.Seed/Program.cs @@ -1,24 +1,24 @@ -using DBSeed; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient; using DbUp; using System.Reflection; +using Database.Seed; string BuildConnectionString() { var server = Environment.GetEnvironmentVariable("DB_SERVER") - ?? throw new InvalidOperationException("DB_SERVER environment variable is not set"); + ?? 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"); + ?? 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"); + ?? 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"); + ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set"); var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") - ?? "True"; + ?? "True"; var builder = new SqlConnectionStringBuilder { @@ -33,6 +33,7 @@ string BuildConnectionString() return builder.ConnectionString; } + try { var connectionString = BuildConnectionString(); @@ -72,7 +73,6 @@ try using (connection) { - ISeeder[] seeders = [ new LocationSeeder(), @@ -96,4 +96,4 @@ catch (Exception ex) Console.Error.WriteLine("Seed failed:"); Console.Error.WriteLine(ex); return 1; -} +} \ No newline at end of file diff --git a/src/Core/Database/Database.Seed/UserSeeder.cs b/src/Core/Database/Database.Seed/UserSeeder.cs index 3dfa52c..b7ae4ba 100644 --- a/src/Core/Database/Database.Seed/UserSeeder.cs +++ b/src/Core/Database/Database.Seed/UserSeeder.cs @@ -1,272 +1,267 @@ using System.Data; using System.Security.Cryptography; using System.Text; -using Domain.Core.Entities; using idunno.Password; using Konscious.Security.Cryptography; using Microsoft.Data.SqlClient; -namespace DBSeed +namespace Database.Seed; + +internal class UserSeeder : ISeeder { - internal class UserSeeder : ISeeder + private static readonly IReadOnlyList<( + string FirstName, + string LastName + )> SeedNames = + [ + ("Aarya", "Mathews"), + ("Aiden", "Wells"), + ("Aleena", "Gonzalez"), + ("Alessandra", "Nelson"), + ("Amari", "Tucker"), + ("Ameer", "Huff"), + ("Amirah", "Hicks"), + ("Analia", "Dominguez"), + ("Anne", "Jenkins"), + ("Apollo", "Davis"), + ("Arianna", "White"), + ("Aubree", "Moore"), + ("Aubrielle", "Raymond"), + ("Aydin", "Odom"), + ("Bowen", "Casey"), + ("Brock", "Huber"), + ("Caiden", "Strong"), + ("Cecilia", "Rosales"), + ("Celeste", "Barber"), + ("Chance", "Small"), + ("Clara", "Roberts"), + ("Collins", "Brandt"), + ("Damir", "Wallace"), + ("Declan", "Crawford"), + ("Dennis", "Decker"), + ("Dylan", "Lang"), + ("Eliza", "Kane"), + ("Elle", "Poole"), + ("Elliott", "Miles"), + ("Emelia", "Lucas"), + ("Emilia", "Simpson"), + ("Emmett", "Lugo"), + ("Ethan", "Stephens"), + ("Etta", "Woods"), + ("Gael", "Moran"), + ("Grant", "Benson"), + ("Gwen", "James"), + ("Huxley", "Chen"), + ("Isabella", "Fisher"), + ("Ivan", "Mathis"), + ("Jamir", "McMillan"), + ("Jaxson", "Shields"), + ("Jimmy", "Richmond"), + ("Josiah", "Flores"), + ("Kaden", "Enriquez"), + ("Kai", "Lawson"), + ("Karsyn", "Adkins"), + ("Karsyn", "Proctor"), + ("Kayden", "Henson"), + ("Kaylie", "Spears"), + ("Kinslee", "Jones"), + ("Kora", "Guerra"), + ("Lane", "Skinner"), + ("Laylani", "Christian"), + ("Ledger", "Carroll"), + ("Leilany", "Small"), + ("Leland", "McCall"), + ("Leonard", "Calhoun"), + ("Levi", "Ochoa"), + ("Lillie", "Vang"), + ("Lola", "Sheppard"), + ("Luciana", "Poole"), + ("Maddox", "Hughes"), + ("Mara", "Blackwell"), + ("Marcellus", "Bartlett"), + ("Margo", "Koch"), + ("Maurice", "Gibson"), + ("Maxton", "Dodson"), + ("Mia", "Parrish"), + ("Millie", "Fuentes"), + ("Nellie", "Villanueva"), + ("Nicolas", "Mata"), + ("Nicolas", "Miller"), + ("Oakleigh", "Foster"), + ("Octavia", "Pierce"), + ("Paisley", "Allison"), + ("Quincy", "Andersen"), + ("Quincy", "Frazier"), + ("Raiden", "Roberts"), + ("Raquel", "Lara"), + ("Rudy", "McIntosh"), + ("Salvador", "Stein"), + ("Samantha", "Dickson"), + ("Solomon", "Richards"), + ("Sylvia", "Hanna"), + ("Talia", "Trujillo"), + ("Thalia", "Farrell"), + ("Trent", "Mayo"), + ("Trinity", "Cummings"), + ("Ty", "Perry"), + ("Tyler", "Romero"), + ("Valeria", "Pierce"), + ("Vance", "Neal"), + ("Whitney", "Bell"), + ("Wilder", "Graves"), + ("William", "Logan"), + ("Zara", "Wilkinson"), + ("Zaria", "Gibson"), + ("Zion", "Watkins"), + ("Zoie", "Armstrong"), + ]; + + public async Task SeedAsync(SqlConnection connection) { - private static readonly IReadOnlyList<( - string FirstName, - string LastName - )> SeedNames = - [ - ("Aarya", "Mathews"), - ("Aiden", "Wells"), - ("Aleena", "Gonzalez"), - ("Alessandra", "Nelson"), - ("Amari", "Tucker"), - ("Ameer", "Huff"), - ("Amirah", "Hicks"), - ("Analia", "Dominguez"), - ("Anne", "Jenkins"), - ("Apollo", "Davis"), - ("Arianna", "White"), - ("Aubree", "Moore"), - ("Aubrielle", "Raymond"), - ("Aydin", "Odom"), - ("Bowen", "Casey"), - ("Brock", "Huber"), - ("Caiden", "Strong"), - ("Cecilia", "Rosales"), - ("Celeste", "Barber"), - ("Chance", "Small"), - ("Clara", "Roberts"), - ("Collins", "Brandt"), - ("Damir", "Wallace"), - ("Declan", "Crawford"), - ("Dennis", "Decker"), - ("Dylan", "Lang"), - ("Eliza", "Kane"), - ("Elle", "Poole"), - ("Elliott", "Miles"), - ("Emelia", "Lucas"), - ("Emilia", "Simpson"), - ("Emmett", "Lugo"), - ("Ethan", "Stephens"), - ("Etta", "Woods"), - ("Gael", "Moran"), - ("Grant", "Benson"), - ("Gwen", "James"), - ("Huxley", "Chen"), - ("Isabella", "Fisher"), - ("Ivan", "Mathis"), - ("Jamir", "McMillan"), - ("Jaxson", "Shields"), - ("Jimmy", "Richmond"), - ("Josiah", "Flores"), - ("Kaden", "Enriquez"), - ("Kai", "Lawson"), - ("Karsyn", "Adkins"), - ("Karsyn", "Proctor"), - ("Kayden", "Henson"), - ("Kaylie", "Spears"), - ("Kinslee", "Jones"), - ("Kora", "Guerra"), - ("Lane", "Skinner"), - ("Laylani", "Christian"), - ("Ledger", "Carroll"), - ("Leilany", "Small"), - ("Leland", "McCall"), - ("Leonard", "Calhoun"), - ("Levi", "Ochoa"), - ("Lillie", "Vang"), - ("Lola", "Sheppard"), - ("Luciana", "Poole"), - ("Maddox", "Hughes"), - ("Mara", "Blackwell"), - ("Marcellus", "Bartlett"), - ("Margo", "Koch"), - ("Maurice", "Gibson"), - ("Maxton", "Dodson"), - ("Mia", "Parrish"), - ("Millie", "Fuentes"), - ("Nellie", "Villanueva"), - ("Nicolas", "Mata"), - ("Nicolas", "Miller"), - ("Oakleigh", "Foster"), - ("Octavia", "Pierce"), - ("Paisley", "Allison"), - ("Quincy", "Andersen"), - ("Quincy", "Frazier"), - ("Raiden", "Roberts"), - ("Raquel", "Lara"), - ("Rudy", "McIntosh"), - ("Salvador", "Stein"), - ("Samantha", "Dickson"), - ("Solomon", "Richards"), - ("Sylvia", "Hanna"), - ("Talia", "Trujillo"), - ("Thalia", "Farrell"), - ("Trent", "Mayo"), - ("Trinity", "Cummings"), - ("Ty", "Perry"), - ("Tyler", "Romero"), - ("Valeria", "Pierce"), - ("Vance", "Neal"), - ("Whitney", "Bell"), - ("Wilder", "Graves"), - ("William", "Logan"), - ("Zara", "Wilkinson"), - ("Zaria", "Gibson"), - ("Zion", "Watkins"), - ("Zoie", "Armstrong"), - ]; + var generator = new PasswordGenerator(); + var rng = new Random(); + int createdUsers = 0; + int createdCredentials = 0; + int createdVerifications = 0; - public async Task SeedAsync(SqlConnection connection) { - var generator = new PasswordGenerator(); - var rng = new Random(); - int createdUsers = 0; - int createdCredentials = 0; - int createdVerifications = 0; + const string firstName = "Test"; + const string lastName = "User"; + const string email = "test.user@thebiergarten.app"; + var dob = new DateTime(1985, 03, 01); + var hash = GeneratePasswordHash("password"); - { - const string firstName = "Test"; - const string lastName = "User"; - const string email = "test.user@thebiergarten.app"; - var dob = new DateTime(1985, 03, 01); - var hash = GeneratePasswordHash("password"); - - await RegisterUserAsync( - connection, - $"{firstName}.{lastName}", - firstName, - lastName, - dob, - email, - hash - ); - } - foreach (var (firstName, lastName) in SeedNames) - { - // prepare user fields - var username = $"{firstName[0]}.{lastName}"; - var email = $"{firstName}.{lastName}@thebiergarten.app"; - var dob = GenerateDateOfBirth(rng); - - // generate a password and hash it - string pwd = generator.Generate( - length: 64, - numberOfDigits: 10, - numberOfSymbols: 10 - ); - string hash = GeneratePasswordHash(pwd); - - - // register the user (creates account + credential) - var id = await RegisterUserAsync( - connection, - username, - firstName, - lastName, - dob, - email, - hash - ); - createdUsers++; - createdCredentials++; - - - - - // add user verification - if (await HasUserVerificationAsync(connection, id)) continue; - - await AddUserVerificationAsync(connection, id); - createdVerifications++; - } - - Console.WriteLine($"Created {createdUsers} user accounts."); - Console.WriteLine($"Added {createdCredentials} user credentials."); - Console.WriteLine($"Added {createdVerifications} user verifications."); - } - - private static async Task RegisterUserAsync( - SqlConnection connection, - string username, - string firstName, - string lastName, - DateTime dateOfBirth, - string email, - string hash - ) - { - await using var command = new SqlCommand("dbo.USP_RegisterUser", connection); - command.CommandType = CommandType.StoredProcedure; - - - command.Parameters.Add("@Username", SqlDbType.VarChar, 64).Value = username; - command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 128).Value = firstName; - command.Parameters.Add("@LastName", SqlDbType.NVarChar, 128).Value = lastName; - command.Parameters.Add("@DateOfBirth", SqlDbType.DateTime).Value = dateOfBirth; - command.Parameters.Add("@Email", SqlDbType.VarChar, 128).Value = email; - command.Parameters.Add("@Hash", SqlDbType.NVarChar, -1).Value = hash; - - var result = await command.ExecuteScalarAsync(); - - - return (Guid)result!; - - } - - private static string GeneratePasswordHash(string pwd) - { - byte[] salt = RandomNumberGenerator.GetBytes(16); - - var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd)) - { - Salt = salt, - DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), - MemorySize = 65536, - Iterations = 4, - }; - - byte[] hash = argon2.GetBytes(32); - return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; - } - - private static async Task HasUserVerificationAsync( - SqlConnection connection, - Guid userAccountId - ) - { - const string sql = """ - SELECT 1 - FROM dbo.UserVerification - WHERE UserAccountId = @UserAccountId; - """; - await using var command = new SqlCommand(sql, connection); - command.Parameters.AddWithValue("@UserAccountId", userAccountId); - var result = await command.ExecuteScalarAsync(); - return result is not null; - } - - private static async Task AddUserVerificationAsync( - SqlConnection connection, - Guid userAccountId - ) - { - await using var command = new SqlCommand( - "dbo.USP_CreateUserVerification", - connection + await RegisterUserAsync( + connection, + $"{firstName}.{lastName}", + firstName, + lastName, + dob, + email, + hash ); - command.CommandType = CommandType.StoredProcedure; - command.Parameters.AddWithValue("@UserAccountID_", userAccountId); - - await command.ExecuteNonQueryAsync(); } - - private static DateTime GenerateDateOfBirth(Random random) + foreach (var (firstName, lastName) in SeedNames) { - int age = 19 + random.Next(0, 30); - DateTime baseDate = DateTime.UtcNow.Date.AddYears(-age); - int offsetDays = random.Next(0, 365); - return baseDate.AddDays(-offsetDays); + // prepare user fields + var username = $"{firstName[0]}.{lastName}"; + var email = $"{firstName}.{lastName}@thebiergarten.app"; + var dob = GenerateDateOfBirth(rng); + + // generate a password and hash it + string pwd = generator.Generate( + length: 64, + numberOfDigits: 10, + numberOfSymbols: 10 + ); + string hash = GeneratePasswordHash(pwd); + + + // register the user (creates account + credential) + var id = await RegisterUserAsync( + connection, + username, + firstName, + lastName, + dob, + email, + hash + ); + createdUsers++; + createdCredentials++; + + + // add user verification + if (await HasUserVerificationAsync(connection, id)) continue; + + await AddUserVerificationAsync(connection, id); + createdVerifications++; } + + Console.WriteLine($"Created {createdUsers} user accounts."); + Console.WriteLine($"Added {createdCredentials} user credentials."); + Console.WriteLine($"Added {createdVerifications} user verifications."); } -} + + private static async Task RegisterUserAsync( + SqlConnection connection, + string username, + string firstName, + string lastName, + DateTime dateOfBirth, + string email, + string hash + ) + { + await using var command = new SqlCommand("dbo.USP_RegisterUser", connection); + command.CommandType = CommandType.StoredProcedure; + + + command.Parameters.Add("@Username", SqlDbType.VarChar, 64).Value = username; + command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 128).Value = firstName; + command.Parameters.Add("@LastName", SqlDbType.NVarChar, 128).Value = lastName; + command.Parameters.Add("@DateOfBirth", SqlDbType.DateTime).Value = dateOfBirth; + command.Parameters.Add("@Email", SqlDbType.VarChar, 128).Value = email; + command.Parameters.Add("@Hash", SqlDbType.NVarChar, -1).Value = hash; + + var result = await command.ExecuteScalarAsync(); + + + return (Guid)result!; + } + + private static string GeneratePasswordHash(string pwd) + { + byte[] salt = RandomNumberGenerator.GetBytes(16); + + var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd)) + { + Salt = salt, + DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), + MemorySize = 65536, + Iterations = 4, + }; + + byte[] hash = argon2.GetBytes(32); + return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}"; + } + + private static async Task HasUserVerificationAsync( + SqlConnection connection, + Guid userAccountId + ) + { + const string sql = """ + SELECT 1 + FROM dbo.UserVerification + WHERE UserAccountId = @UserAccountId; + """; + await using var command = new SqlCommand(sql, connection); + command.Parameters.AddWithValue("@UserAccountId", userAccountId); + var result = await command.ExecuteScalarAsync(); + return result is not null; + } + + private static async Task AddUserVerificationAsync( + SqlConnection connection, + Guid userAccountId + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateUserVerification", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@UserAccountID_", userAccountId); + + await command.ExecuteNonQueryAsync(); + } + + private static DateTime GenerateDateOfBirth(Random random) + { + int age = 19 + random.Next(0, 30); + DateTime baseDate = DateTime.UtcNow.Date.AddYears(-age); + int offsetDays = random.Next(0, 365); + return baseDate.AddDays(-offsetDays); + } +} \ No newline at end of file diff --git a/src/Core/Domain/Entities/UserAccount.cs b/src/Core/Domain/Entities/UserAccount.cs index 4f10fdd..bda19c7 100644 --- a/src/Core/Domain/Entities/UserAccount.cs +++ b/src/Core/Domain/Entities/UserAccount.cs @@ -1,4 +1,4 @@ -namespace Domain.Core.Entities; +namespace Domain.Entities; public class UserAccount { diff --git a/src/Core/Domain/Entities/UserCredential.cs b/src/Core/Domain/Entities/UserCredential.cs index 1dd87f8..65b46a1 100644 --- a/src/Core/Domain/Entities/UserCredential.cs +++ b/src/Core/Domain/Entities/UserCredential.cs @@ -1,4 +1,4 @@ -namespace Domain.Core.Entities; +namespace Domain.Entities; public class UserCredential { diff --git a/src/Core/Domain/Entities/UserVerification.cs b/src/Core/Domain/Entities/UserVerification.cs index 777203c..3e66754 100644 --- a/src/Core/Domain/Entities/UserVerification.cs +++ b/src/Core/Domain/Entities/UserVerification.cs @@ -1,4 +1,4 @@ -namespace Domain.Core.Entities; +namespace Domain.Entities; public class UserVerification { diff --git a/src/Core/Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj b/src/Core/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj similarity index 100% rename from src/Core/Infrastructure/Infrastructure.Repository.Tests/Repository.Tests.csproj rename to src/Core/Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs index 65a5501..4672852 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/AuthRepository.cs @@ -1,18 +1,15 @@ using System.Data; using System.Data.Common; -using Domain.Core.Entities; +using Domain.Entities; using Infrastructure.Repository.Sql; namespace Infrastructure.Repository.Auth; -public class AuthRepository - : Repository, +public class AuthRepository(ISqlConnectionFactory connectionFactory) + : Repository(connectionFactory), IAuthRepository { - public AuthRepository(ISqlConnectionFactory connectionFactory) - : base(connectionFactory) { } - - public async Task RegisterUserAsync( + public async Task RegisterUserAsync( string username, string firstName, string lastName, @@ -37,7 +34,7 @@ public class AuthRepository var result = await command.ExecuteScalarAsync(); var userAccountId = result != null ? (Guid)result : Guid.Empty; - return new Domain.Core.Entities.UserAccount + return new Domain.Entities.UserAccount { UserAccountId = userAccountId, Username = username, @@ -49,7 +46,7 @@ public class AuthRepository }; } - public async Task GetUserByEmailAsync( + public async Task GetUserByEmailAsync( string email ) { @@ -64,7 +61,7 @@ public class AuthRepository return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public async Task GetUserByUsernameAsync( + public async Task GetUserByUsernameAsync( string username ) { @@ -115,11 +112,11 @@ public class AuthRepository /// /// Maps a data reader row to a UserAccount entity. /// - protected override Domain.Core.Entities.UserAccount MapToEntity( + protected override Domain.Entities.UserAccount MapToEntity( DbDataReader reader ) { - return new Domain.Core.Entities.UserAccount + return new Domain.Entities.UserAccount { UserAccountId = reader.GetGuid( reader.GetOrdinal("UserAccountId") diff --git a/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs index 619276d..4a57360 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/Auth/IAuthRepository.cs @@ -1,4 +1,4 @@ -using Domain.Core.Entities; +using Domain.Entities; namespace Infrastructure.Repository.Auth; @@ -18,7 +18,7 @@ public interface IAuthRepository /// User's date of birth /// Hashed password /// The newly created UserAccount with generated ID - Task RegisterUserAsync( + Task RegisterUserAsync( string username, string firstName, string lastName, @@ -33,7 +33,7 @@ public interface IAuthRepository /// /// Email address to search for /// UserAccount if found, null otherwise - Task GetUserByEmailAsync( + Task GetUserByEmailAsync( string email ); @@ -43,7 +43,7 @@ public interface IAuthRepository /// /// Username to search for /// UserAccount if found, null otherwise - Task GetUserByUsernameAsync( + Task GetUserByUsernameAsync( string username ); diff --git a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs index e1929ca..774f825 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/IUserAccountRepository.cs @@ -2,15 +2,15 @@ namespace Infrastructure.Repository.UserAccount; public interface IUserAccountRepository { - Task GetByIdAsync(Guid id); - Task> GetAllAsync( + Task GetByIdAsync(Guid id); + Task> GetAllAsync( int? limit, int? offset ); - Task UpdateAsync(Domain.Core.Entities.UserAccount userAccount); + Task UpdateAsync(Domain.Entities.UserAccount userAccount); Task DeleteAsync(Guid id); - Task GetByUsernameAsync( + Task GetByUsernameAsync( string username ); - Task GetByEmailAsync(string email); + Task GetByEmailAsync(string email); } \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs index 6d9252b..d07fa3c 100644 --- a/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs +++ b/src/Core/Infrastructure/Infrastructure.Repository/UserAccount/UserAccountRepository.cs @@ -5,10 +5,10 @@ using Infrastructure.Repository.Sql; namespace Infrastructure.Repository.UserAccount; public class UserAccountRepository(ISqlConnectionFactory connectionFactory) - : Repository(connectionFactory), + : Repository(connectionFactory), IUserAccountRepository { - public async Task GetByIdAsync( + public async Task GetByIdAsync( Guid id ) { @@ -24,7 +24,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) } public async Task< - IEnumerable + IEnumerable > GetAllAsync(int? limit, int? offset) { await using var connection = await CreateConnection(); @@ -39,7 +39,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) AddParameter(command, "@Offset", offset.Value); await using var reader = await command.ExecuteReaderAsync(); - var users = new List(); + var users = new List(); while (await reader.ReadAsync()) { @@ -50,7 +50,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) } public async Task UpdateAsync( - Domain.Core.Entities.UserAccount userAccount + Domain.Entities.UserAccount userAccount ) { await using var connection = await CreateConnection(); @@ -79,7 +79,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) await command.ExecuteNonQueryAsync(); } - public async Task GetByUsernameAsync( + public async Task GetByUsernameAsync( string username ) { @@ -94,7 +94,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) return await reader.ReadAsync() ? MapToEntity(reader) : null; } - public async Task GetByEmailAsync( + public async Task GetByEmailAsync( string email ) { @@ -109,11 +109,11 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory) return await reader.ReadAsync() ? MapToEntity(reader) : null; } - protected override Domain.Core.Entities.UserAccount MapToEntity( + protected override Domain.Entities.UserAccount MapToEntity( DbDataReader reader ) { - return new Domain.Core.Entities.UserAccount + return new Domain.Entities.UserAccount { UserAccountId = reader.GetGuid( reader.GetOrdinal("UserAccountId") diff --git a/src/Core/Service/Service.Core/Auth/AuthService.cs b/src/Core/Service/Service.Core/Auth/AuthService.cs index 738a0ae..8756dc3 100644 --- a/src/Core/Service/Service.Core/Auth/AuthService.cs +++ b/src/Core/Service/Service.Core/Auth/AuthService.cs @@ -1,4 +1,4 @@ -using Domain.Core.Entities; +using Domain.Entities; using Infrastructure.PasswordHashing; using Infrastructure.Repository.Auth; diff --git a/src/Core/Service/Service.Core/Auth/IAuthService.cs b/src/Core/Service/Service.Core/Auth/IAuthService.cs index 3e6e17c..8e99efb 100644 --- a/src/Core/Service/Service.Core/Auth/IAuthService.cs +++ b/src/Core/Service/Service.Core/Auth/IAuthService.cs @@ -1,4 +1,4 @@ -using Domain.Core.Entities; +using Domain.Entities; namespace Service.Core.Auth; diff --git a/src/Core/Service/Service.Core/User/IUserService.cs b/src/Core/Service/Service.Core/User/IUserService.cs index 3bed122..a515cab 100644 --- a/src/Core/Service/Service.Core/User/IUserService.cs +++ b/src/Core/Service/Service.Core/User/IUserService.cs @@ -1,4 +1,4 @@ -using Domain.Core.Entities; +using Domain.Entities; namespace Service.Core.User; diff --git a/src/Core/Service/Service.Core/User/UserService.cs b/src/Core/Service/Service.Core/User/UserService.cs index 977c94c..1959553 100644 --- a/src/Core/Service/Service.Core/User/UserService.cs +++ b/src/Core/Service/Service.Core/User/UserService.cs @@ -1,4 +1,4 @@ -using Domain.Core.Entities; +using Domain.Entities; using Infrastructure.Repository.UserAccount; namespace Service.Core.User; From 2cb8f1d91807f7f21ac16d1ab90234087343b49e Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 12 Feb 2026 18:05:44 -0500 Subject: [PATCH 7/7] Update ISeeder.cs --- src/Core/Database/Database.Seed/ISeeder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Core/Database/Database.Seed/ISeeder.cs b/src/Core/Database/Database.Seed/ISeeder.cs index b3a414e..bd0d18d 100644 --- a/src/Core/Database/Database.Seed/ISeeder.cs +++ b/src/Core/Database/Database.Seed/ISeeder.cs @@ -1,6 +1,8 @@ using Microsoft.Data.SqlClient; -namespace DBSeed +namespace Database.Seed; + +internal interface ISeeder { Task SeedAsync(SqlConnection connection); } \ No newline at end of file