diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml index 75c228e..460aa03 100644 --- a/.github/workflows/dependency-updates.yml +++ b/.github/workflows/dependency-updates.yml @@ -53,7 +53,7 @@ jobs: branch: "update/${{ env.date }}" labels: dependencies delete-branch: true - commit-message: "Update packages" + commit-message: "chore: Update dependencies" assignees: ${{ env.assignee }} base: ${{ env.baseBranch }} title: "Automatic Package Update ${{ env.date }}" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 1a043db..0c6027b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,4 +1,4 @@ -name: CI +name: CI/CD on: workflow_dispatch: @@ -7,25 +7,59 @@ on: pull_request: branches: [ main ] +env: + DOCKER_IMAGE: alexdev28/clean-architecture + jobs: build: - runs-on: ubuntu-latest + env: + solutionFile: CleanArchitecture.sln + projectName: CleanArchitecture steps: - uses: actions/checkout@v3 - - name: Dependency Review - uses: actions/dependency-review-action@v3 - with: - base-ref: ${{ github.event.pull_request.base.sha || 'main' }} - head-ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.x.x + dotnet-version: | + 8.x.x + # 6.x.x + # - name: Check for vulnerabilities + # run: | + # VERSION=$(curl -s https://jeremylong.github.io/DependencyCheck/current.txt) + # curl -Ls "https://github.com/jeremylong/DependencyCheck/releases/download/v$VERSION/dependency-check-$VERSION-release.zip" --output dependency-check.zip + # unzip dependency-check.zip + # ./dependency-check/bin/dependency-check.sh -s "**/*.csproj" --project "${{ env.projectName }}" --failOnCVSS "7" + # - name: Check for license issues + # run: | + # dotnet new tool-manifest + # dotnet tool install --local liz.tool + # dotnet liz "${{ env.solutionFile }}" --suppress-progressbar - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore - name: Test run: dotnet test --no-build --verbosity normal + + docker: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + # platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ env.DOCKER_IMAGE }}:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0899f53..ffb2b90 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ riderModule.iml .idea .vs CleanArchitecture.sln.DotSettings.user +.DS_Store \ No newline at end of file diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 7fb8080..9769426 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -9,18 +9,18 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/CleanArchitecture.Api/Controllers/TenantController.cs b/CleanArchitecture.Api/Controllers/TenantController.cs index 4a312eb..8caf488 100644 --- a/CleanArchitecture.Api/Controllers/TenantController.cs +++ b/CleanArchitecture.Api/Controllers/TenantController.cs @@ -37,7 +37,8 @@ public sealed class TenantController : ApiController [FromQuery] PageQuery query, [FromQuery] string searchTerm = "", [FromQuery] bool includeDeleted = false, - [FromQuery, SortableFieldsAttribute] SortQuery? sortQuery = null) + [FromQuery] [SortableFieldsAttribute] + SortQuery? sortQuery = null) { var tenants = await _tenantService.GetAllTenantsAsync( query, diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 6e12fea..9a0f9e3 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -37,7 +37,8 @@ public sealed class UserController : ApiController [FromQuery] PageQuery query, [FromQuery] string searchTerm = "", [FromQuery] bool includeDeleted = false, - [FromQuery, SortableFieldsAttribute] SortQuery? sortQuery = null) + [FromQuery] [SortableFieldsAttribute] + SortQuery? sortQuery = null) { var users = await _userService.GetAllUsersAsync( query, diff --git a/CleanArchitecture.Api/Swagger/SortableFieldsAttribute.cs b/CleanArchitecture.Api/Swagger/SortableFieldsAttribute.cs index 83cca2d..9774efd 100644 --- a/CleanArchitecture.Api/Swagger/SortableFieldsAttribute.cs +++ b/CleanArchitecture.Api/Swagger/SortableFieldsAttribute.cs @@ -4,7 +4,7 @@ using CleanArchitecture.Application.ViewModels.Sorting; namespace CleanArchitecture.Api.Swagger; -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +[AttributeUsage(AttributeTargets.Parameter)] public sealed class SortableFieldsAttribute : SwaggerSortableFieldsAttribute where TSortingProvider : ISortingExpressionProvider, new() diff --git a/CleanArchitecture.Api/Swagger/SortableFieldsAttributeFilter.cs b/CleanArchitecture.Api/Swagger/SortableFieldsAttributeFilter.cs index d628229..6dd403c 100644 --- a/CleanArchitecture.Api/Swagger/SortableFieldsAttributeFilter.cs +++ b/CleanArchitecture.Api/Swagger/SortableFieldsAttributeFilter.cs @@ -26,7 +26,6 @@ public sealed class SortableFieldsAttributeFilter : IParameterFilter var description = string.Join("
", attribute.GetFields().Order()); parameter.Description = $"{parameter.Description}

" + - $"**Allowed values:**
{description}"; + $"**Allowed values:**
{description}"; } -} - +} \ No newline at end of file diff --git a/CleanArchitecture.Api/Swagger/SwaggerSortableFieldsAttribute.cs b/CleanArchitecture.Api/Swagger/SwaggerSortableFieldsAttribute.cs index 1dff29d..e3545a5 100644 --- a/CleanArchitecture.Api/Swagger/SwaggerSortableFieldsAttribute.cs +++ b/CleanArchitecture.Api/Swagger/SwaggerSortableFieldsAttribute.cs @@ -6,4 +6,4 @@ namespace CleanArchitecture.Api.Swagger; public abstract class SwaggerSortableFieldsAttribute : Attribute { public abstract IEnumerable GetFields(); -} +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj index b169280..46c6cda 100644 --- a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj +++ b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj @@ -8,12 +8,12 @@ - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,8 +24,8 @@ - - + + diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs index e5d0622..47a35ea 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Tenants/GetTenantByIdTestFixture.cs @@ -1,9 +1,7 @@ using System; -using System.Collections.Generic; using CleanArchitecture.Application.Queries.Tenants.GetTenantById; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces.Repositories; -using MockQueryable.NSubstitute; using NSubstitute; namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants; @@ -30,9 +28,11 @@ public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture { tenant.Delete(); } + else + { + TenantRepository.GetByIdAsync(Arg.Is(y => y == tenant.Id)).Returns(tenant); + } - var tenantList = new List { tenant }.BuildMock(); - TenantRepository.GetAllNoTracking().Returns(tenantList); return tenant; } diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs index 9464fed..60305bf 100644 --- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -35,7 +35,7 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture var query = new[] { user }.BuildMock(); - UserRepository.GetAllNoTracking().Returns(query); + UserRepository.GetByIdAsync(Arg.Is(y => y == ExistingUserId)).Returns(user); } public void SetupDeletedUserAsync() diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index c251147..4e70f41 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -6,12 +6,12 @@ - + - - + + diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs index a097c13..9f46f18 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -32,7 +32,8 @@ public sealed class GetAllTenantsQueryHandler : { var tenantsQuery = _tenantRepository .GetAllNoTracking() - .Include(x => x.Users) + .IgnoreQueryFilters() + .Include(x => x.Users.Where(y => request.IncludeDeleted || !y.Deleted)) .Where(x => request.IncludeDeleted || !x.Deleted); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs index 652c458..ddc25bf 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels.Tenants; @@ -7,7 +6,6 @@ using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Notifications; using MediatR; -using Microsoft.EntityFrameworkCore; namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; @@ -25,10 +23,7 @@ public sealed class GetTenantByIdQueryHandler : public async Task Handle(GetTenantByIdQuery request, CancellationToken cancellationToken) { - var tenant = _tenantRepository - .GetAllNoTracking() - .Include(x => x.Users) - .FirstOrDefault(x => x.Id == request.TenantId && !x.Deleted); + var tenant = await _tenantRepository.GetByIdAsync(request.TenantId); if (tenant is null) { diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs index 18f2df3..7de025a 100644 --- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs @@ -32,6 +32,7 @@ public sealed class GetAllUsersQueryHandler : { var usersQuery = _userRepository .GetAllNoTracking() + .IgnoreQueryFilters() .Where(x => request.IncludeDeleted || !x.Deleted); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs index f0f2b77..d80268a 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Domain.Errors; @@ -24,9 +23,7 @@ public sealed class GetUserByIdQueryHandler : public async Task Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { - var user = _userRepository - .GetAllNoTracking() - .FirstOrDefault(x => x.Id == request.UserId && !x.Deleted); + var user = await _userRepository.GetByIdAsync(request.UserId); if (user is null) { diff --git a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs index 414206c..97679ac 100644 --- a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs @@ -34,6 +34,7 @@ public sealed class TenantsApiImplementation : TenantsApi.TenantsApiBase var tenants = await _tenantRepository .GetAllNoTracking() + .IgnoreQueryFilters() .Where(tenant => idsAsGuids.Contains(tenant.Id)) .Select(tenant => new Tenant { diff --git a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs index ff872e1..6255920 100644 --- a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs @@ -34,6 +34,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase var users = await _userRepository .GetAllNoTracking() + .IgnoreQueryFilters() .Where(user => idsAsGuids.Contains(user.Id)) .Select(user => new GrpcUser { diff --git a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj index e1ae055..5f03e8c 100644 --- a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj +++ b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj @@ -8,12 +8,12 @@ - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,7 +24,7 @@ - + diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs index 0ac8c00..b44cb19 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Shared.Events.Tenant; @@ -11,13 +12,13 @@ public sealed class CreateTenantCommandHandlerTests private readonly CreateTenantCommandTestFixture _fixture = new(); [Fact] - public void Should_Create_Tenant() + public async Task Should_Create_Tenant() { var command = new CreateTenantCommand( Guid.NewGuid(), "Test Tenant"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoDomainNotification() @@ -28,7 +29,7 @@ public sealed class CreateTenantCommandHandlerTests } [Fact] - public void Should_Not_Create_Tenant_Insufficient_Permissions() + public async Task Should_Not_Create_Tenant_Insufficient_Permissions() { _fixture.SetupUser(); @@ -36,7 +37,7 @@ public sealed class CreateTenantCommandHandlerTests Guid.NewGuid(), "Test Tenant"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -48,7 +49,7 @@ public sealed class CreateTenantCommandHandlerTests } [Fact] - public void Should_Not_Create_Tenant_Already_Exists() + public async Task Should_Not_Create_Tenant_Already_Exists() { var command = new CreateTenantCommand( Guid.NewGuid(), @@ -56,7 +57,7 @@ public sealed class CreateTenantCommandHandlerTests _fixture.SetupExistingTenant(command.AggregateId); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs index 68134d9..b3e5579 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/DeleteTenant/DeleteTenantCommandHandlerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Shared.Events.Tenant; @@ -11,13 +12,13 @@ public sealed class DeleteTenantCommandHandlerTests private readonly DeleteTenantCommandTestFixture _fixture = new(); [Fact] - public void Should_Delete_Tenant() + public async Task Should_Delete_Tenant() { var tenant = _fixture.SetupTenant(); var command = new DeleteTenantCommand(tenant.Id); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoDomainNotification() @@ -26,13 +27,13 @@ public sealed class DeleteTenantCommandHandlerTests } [Fact] - public void Should_Not_Delete_Non_Existing_Tenant() + public async Task Should_Not_Delete_Non_Existing_Tenant() { _fixture.SetupTenant(); var command = new DeleteTenantCommand(Guid.NewGuid()); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -44,14 +45,14 @@ public sealed class DeleteTenantCommandHandlerTests } [Fact] - public void Should_Not_Delete_Tenant_Insufficient_Permissions() + public async Task Should_Not_Delete_Tenant_Insufficient_Permissions() { var tenant = _fixture.SetupTenant(); _fixture.SetupUser(); var command = new DeleteTenantCommand(tenant.Id); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs index 980b97b..e03eee0 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/UpdateTenant/UpdateTenantCommandHandlerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Shared.Events.Tenant; @@ -11,7 +12,7 @@ public sealed class UpdateTenantCommandHandlerTests private readonly UpdateTenantCommandTestFixture _fixture = new(); [Fact] - public void Should_Update_Tenant() + public async Task Should_Update_Tenant() { var command = new UpdateTenantCommand( Guid.NewGuid(), @@ -19,7 +20,7 @@ public sealed class UpdateTenantCommandHandlerTests _fixture.SetupExistingTenant(command.AggregateId); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyCommit() @@ -30,7 +31,7 @@ public sealed class UpdateTenantCommandHandlerTests } [Fact] - public void Should_Not_Update_Tenant_Insufficient_Permissions() + public async Task Should_Not_Update_Tenant_Insufficient_Permissions() { var command = new UpdateTenantCommand( Guid.NewGuid(), @@ -38,7 +39,7 @@ public sealed class UpdateTenantCommandHandlerTests _fixture.SetupUser(); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -50,13 +51,13 @@ public sealed class UpdateTenantCommandHandlerTests } [Fact] - public void Should_Not_Update_Tenant_Not_Existing() + public async Task Should_Not_Update_Tenant_Not_Existing() { var command = new UpdateTenantCommand( Guid.NewGuid(), "Tenant Name"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index ef81bc5..25108ee 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; @@ -13,7 +14,7 @@ public sealed class CreateUserCommandHandlerTests private readonly CreateUserCommandTestFixture _fixture = new(); [Fact] - public void Should_Create_User() + public async Task Should_Create_User() { _fixture.SetupCurrentUser(); @@ -28,7 +29,7 @@ public sealed class CreateUserCommandHandlerTests "Email", "Po=PF]PC6t.?8?ks)A6W"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoDomainNotification() @@ -37,7 +38,7 @@ public sealed class CreateUserCommandHandlerTests } [Fact] - public void Should_Not_Create_Already_Existing_User() + public async Task Should_Not_Create_Already_Existing_User() { _fixture.SetupCurrentUser(); @@ -51,7 +52,7 @@ public sealed class CreateUserCommandHandlerTests "Email", "Po=PF]PC6t.?8?ks)A6W"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -63,7 +64,7 @@ public sealed class CreateUserCommandHandlerTests } [Fact] - public void Should_Not_Create_Already_Existing_Email() + public async Task Should_Not_Create_Already_Existing_Email() { _fixture.SetupCurrentUser(); @@ -86,7 +87,7 @@ public sealed class CreateUserCommandHandlerTests "Email", "Po=PF]PC6t.?8?ks)A6W"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -98,7 +99,7 @@ public sealed class CreateUserCommandHandlerTests } [Fact] - public void Should_Not_Create_User_Tenant_Does_Not_Exist() + public async Task Should_Not_Create_User_Tenant_Does_Not_Exist() { _fixture.SetupCurrentUser(); @@ -112,7 +113,7 @@ public sealed class CreateUserCommandHandlerTests "Email", "Po=PF]PC6t.?8?ks)A6W"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -124,7 +125,7 @@ public sealed class CreateUserCommandHandlerTests } [Fact] - public void Should_Not_Create_User_Insufficient_Permissions() + public async Task Should_Not_Create_User_Insufficient_Permissions() { _fixture.SetupUser(); @@ -136,7 +137,7 @@ public sealed class CreateUserCommandHandlerTests "Email", "Po=PF]PC6t.?8?ks)A6W"); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs index 7dc1c8d..c61ddb6 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Users.DeleteUser; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Shared.Events.User; @@ -11,13 +12,13 @@ public sealed class DeleteUserCommandHandlerTests private readonly DeleteUserCommandTestFixture _fixture = new(); [Fact] - public void Should_Delete_User() + public async Task Should_Delete_User() { var user = _fixture.SetupUser(); var command = new DeleteUserCommand(user.Id); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoDomainNotification() @@ -26,13 +27,13 @@ public sealed class DeleteUserCommandHandlerTests } [Fact] - public void Should_Not_Delete_Non_Existing_User() + public async Task Should_Not_Delete_Non_Existing_User() { _fixture.SetupUser(); var command = new DeleteUserCommand(Guid.NewGuid()); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() @@ -44,7 +45,7 @@ public sealed class DeleteUserCommandHandlerTests } [Fact] - public void Should_Not_Delete_User_Insufficient_Permissions() + public async Task Should_Not_Delete_User_Insufficient_Permissions() { var user = _fixture.SetupUser(); @@ -52,7 +53,7 @@ public sealed class DeleteUserCommandHandlerTests var command = new DeleteUserCommand(user.Id); - _fixture.CommandHandler.Handle(command, default).Wait(); + await _fixture.CommandHandler.Handle(command, default); _fixture .VerifyNoCommit() diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index 1a8efa5..0a38606 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -6,20 +6,20 @@ - - - - - - - + + + + + + + - + - + diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs index 2a72fae..118a211 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -20,7 +20,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser; public sealed class LoginUserCommandHandler : CommandHandlerBase, IRequestHandler { - private const double _expiryDurationMinutes = 30; + private const double _expiryDurationMinutes = 60; private readonly TokenSettings _tokenSettings; private readonly IUserRepository _userRepository; diff --git a/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs b/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs index 81cb7bc..43732ab 100644 --- a/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs +++ b/CleanArchitecture.Domain/Rabbitmq/RabbitMqHandler.cs @@ -52,7 +52,8 @@ public sealed class RabbitMqHandler : BackgroundService { if (!_configuration.Enabled) { - _logger.LogInformation("RabbitMQ is disabled. Skipping the creation of exchange {exchangeName}.", exchangeName); + _logger.LogInformation("RabbitMQ is disabled. Skipping the creation of exchange {exchangeName}.", + exchangeName); return; } diff --git a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj index c2b987a..36d4546 100644 --- a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj +++ b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ - + diff --git a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj index 7bc9f1b..a20637f 100644 --- a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj +++ b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj @@ -6,20 +6,20 @@ - - + + - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs index 6a5517f..1b8258a 100644 --- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs @@ -1,10 +1,11 @@ +using System.Linq; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Infrastructure.Configurations; using Microsoft.EntityFrameworkCore; namespace CleanArchitecture.Infrastructure.Database; -public class ApplicationDbContext : DbContext +public partial class ApplicationDbContext : DbContext { public DbSet Users { get; set; } = null!; public DbSet Tenants { get; set; } = null!; @@ -14,6 +15,29 @@ public class ApplicationDbContext : DbContext } protected override void OnModelCreating(ModelBuilder builder) + { + foreach (var entity in builder.Model.GetEntityTypes()) + { + if (entity.ClrType.GetProperty(DbContextUtility.IsDeletedProperty) is not null) + { + builder.Entity(entity.ClrType) + .HasQueryFilter(DbContextUtility.GetIsDeletedRestriction(entity.ClrType)); + } + } + + base.OnModelCreating(builder); + + ApplyConfigurations(builder); + + // Make referential delete behaviour restrict instead of cascade for everything + foreach (var relationship in builder.Model.GetEntityTypes() + .SelectMany(x => x.GetForeignKeys())) + { + relationship.DeleteBehavior = DeleteBehavior.Restrict; + } + } + + private static void ApplyConfigurations(ModelBuilder builder) { builder.ApplyConfiguration(new UserConfiguration()); builder.ApplyConfiguration(new TenantConfiguration()); diff --git a/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs b/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs new file mode 100644 index 0000000..29900c4 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Infrastructure.Database; + +public partial class ApplicationDbContext +{ + public static class DbContextUtility + { + public const string IsDeletedProperty = "Deleted"; + + public static readonly MethodInfo PropertyMethod = typeof(EF) + .GetMethod(nameof(EF.Property), BindingFlags.Static | BindingFlags.Public) + !.MakeGenericMethod(typeof(bool)); + + public static LambdaExpression GetIsDeletedRestriction(Type type) + { + var parm = Expression.Parameter(type, "it"); + var prop = Expression.Call(PropertyMethod, parm, Expression.Constant(IsDeletedProperty)); + var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(false)); + var lambda = Expression.Lambda(condition, parm); + return lambda; + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Migrations/20231001125849_DeletedQueryFilter.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20231001125849_DeletedQueryFilter.Designer.cs new file mode 100644 index 0000000..356344d --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20231001125849_DeletedQueryFilter.Designer.cs @@ -0,0 +1,138 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20231001125849_DeletedQueryFilter")] + partial class DeletedQueryFilter + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.11") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Id"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + Deleted = false, + Name = "Admin Tenant" + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastLoggedinDate") + .HasColumnType("datetimeoffset"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + Deleted = false, + Email = "admin@email.com", + FirstName = "Admin", + LastName = "User", + Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + Role = 0, + Status = 0, + TenantId = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a") + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.HasOne("CleanArchitecture.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/20231001125849_DeletedQueryFilter.cs b/CleanArchitecture.Infrastructure/Migrations/20231001125849_DeletedQueryFilter.cs new file mode 100644 index 0000000..752b0af --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20231001125849_DeletedQueryFilter.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class DeletedQueryFilter : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users"); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users"); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 831c467..cdbe611 100644 --- a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace CleanArchitecture.Infrastructure.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("ProductVersion", "7.0.11") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -119,7 +119,7 @@ namespace CleanArchitecture.Infrastructure.Migrations b.HasOne("CleanArchitecture.Domain.Entities.Tenant", "Tenant") .WithMany("Users") .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.Navigation("Tenant"); diff --git a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj index 20da691..f53a280 100644 --- a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj +++ b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj @@ -8,15 +8,15 @@ - - - - - - - - - + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -27,8 +27,8 @@ - - + + diff --git a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs index d765819..5b5abfd 100644 --- a/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs +++ b/CleanArchitecture.IntegrationTests/Controller/TenantControllerTests.cs @@ -37,6 +37,8 @@ public sealed class TenantControllerTests : IClassFixture message!.Data!.Id.Should().Be(_fixture.CreatedTenantId); message.Data.Name.Should().Be("Test Tenant"); + + message.Data.Users.Count().Should().Be(1); } [Fact] @@ -55,6 +57,10 @@ public sealed class TenantControllerTests : IClassFixture message.Data!.Items .FirstOrDefault(x => x.Id == _fixture.CreatedTenantId) .Should().NotBeNull(); + + message.Data.Items + .FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)! + .Users.Count().Should().Be(1); } [Fact] diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs index 3d66346..0d9a821 100644 --- a/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs +++ b/CleanArchitecture.IntegrationTests/Fixtures/TenantTestFixture.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; using CleanArchitecture.Infrastructure.Database; namespace CleanArchitecture.IntegrationTests.Fixtures; @@ -16,6 +17,15 @@ public sealed class TenantTestFixture : TestFixtureBase CreatedTenantId, "Test Tenant")); + context.Users.Add(new User( + Guid.NewGuid(), + CreatedTenantId, + "test@user.de", + "test", + "user", + "Test User", + UserRole.User)); + context.SaveChanges(); } } \ No newline at end of file diff --git a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj index 2a17507..b89928d 100644 --- a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj +++ b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj @@ -6,16 +6,16 @@ - - - - + + + + - - - + + + diff --git a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj index 710ccb2..5f363a3 100644 --- a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj +++ b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj @@ -8,12 +8,12 @@ - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,9 +24,9 @@ - - - + + + diff --git a/Readme.md b/Readme.md index a2775a8..d2c35c5 100644 --- a/Readme.md +++ b/Readme.md @@ -57,15 +57,13 @@ Running the container ### Using docker-compose -1. Change the ConnectionString in the appsettings.json to `Server=db;Database=clean-architecture;Trusted_Connection=False;MultipleActiveResultSets=true;TrustServerCertificate=True;User Id=SA;Password=Password123!#` -2. Build the Dockerfile: `docker build -t clean-architecture .` -3. Running the docker compose: `docker-compose up -d` (Delete: `docker-compose down`) +1. Build the Dockerfile: `docker build -t clean-architecture .` +2. Running the docker compose: `docker-compose up -d` (Delete: `docker-compose down`) ### Using Kubernetes -1. Change the ConnectionString in the appsettings.json to `Server=sql-server;Database=clean-architecture;Trusted_Connection=False;MultipleActiveResultSets=true;TrustServerCertificate=True;User Id=SA;Password=Password123!#` -2. Build the docker image and push it to the docker hub (Change the image name in the `k8s-deployment.yml` to your own) -Apply the deployment file: `kubectl apply -f k8s-deployment.yml` (Delete: `kubectl delete -f k8s-deployment.yml`) +1. Build the docker image and push it to the docker hub (Change the image name in the `k8s-deployment.yml` to your own) +2. Apply the deployment file: `kubectl apply -f k8s-deployment.yml` (Delete: `kubectl delete -f k8s-deployment.yml`) ## Running the Tests diff --git a/docker-compose.yml b/docker-compose.yml index 9bafb9c..5ad1a77 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: condition: service_healthy links: - db + environment: + - ConnectionStrings__DefaultConnection=Server=db;Database=clean-architecture;Trusted_Connection=False;MultipleActiveResultSets=true;TrustServerCertificate=True;User Id=SA;Password=Password123!# healthcheck: test: ["CMD", "curl", "--fail", "http://localhost/healthz"] interval: 30s diff --git a/k8s-deployments/clean-architecture.yml b/k8s-deployments/clean-architecture.yml index 86140d1..bfdbfba 100644 --- a/k8s-deployments/clean-architecture.yml +++ b/k8s-deployments/clean-architecture.yml @@ -31,4 +31,7 @@ spec: # Replace this with the path to your built image image: alexdev28/clean-architecture:latest ports: - - containerPort: 80 \ No newline at end of file + - containerPort: 80 + env: + - name: ConnectionStrings__DefaultConnection + value: Server=sql-server;Database=clean-architecture;Trusted_Connection=False;MultipleActiveResultSets=true;TrustServerCertificate=True;User Id=SA;Password=Password123!# \ No newline at end of file