0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-08-22 19:28:34 +00:00

Merge branch 'main' into feature/dotnet_8

This commit is contained in:
alex289 2023-11-10 16:11:04 +01:00
commit ca81e2f076
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
43 changed files with 442 additions and 154 deletions

View File

@ -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 }}"

View File

@ -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

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ riderModule.iml
.idea
.vs
CleanArchitecture.sln.DotSettings.user
.DS_Store

View File

@ -9,18 +9,18 @@
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.ApplicationStatus" Version="7.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="7.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="7.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="7.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="7.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="7.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.10" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.10" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.13" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.13" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.13" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
</ItemGroup>

View File

@ -37,7 +37,8 @@ public sealed class TenantController : ApiController
[FromQuery] PageQuery query,
[FromQuery] string searchTerm = "",
[FromQuery] bool includeDeleted = false,
[FromQuery, SortableFieldsAttribute<TenantViewModelSortProvider, TenantViewModel, Tenant>] SortQuery? sortQuery = null)
[FromQuery] [SortableFieldsAttribute<TenantViewModelSortProvider, TenantViewModel, Tenant>]
SortQuery? sortQuery = null)
{
var tenants = await _tenantService.GetAllTenantsAsync(
query,

View File

@ -37,7 +37,8 @@ public sealed class UserController : ApiController
[FromQuery] PageQuery query,
[FromQuery] string searchTerm = "",
[FromQuery] bool includeDeleted = false,
[FromQuery, SortableFieldsAttribute<UserViewModelSortProvider, UserViewModel, User>] SortQuery? sortQuery = null)
[FromQuery] [SortableFieldsAttribute<UserViewModelSortProvider, UserViewModel, User>]
SortQuery? sortQuery = null)
{
var users = await _userService.GetAllUsersAsync(
query,

View File

@ -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<TSortingProvider, TViewModel, TEntity>
: SwaggerSortableFieldsAttribute
where TSortingProvider : ISortingExpressionProvider<TViewModel, TEntity>, new()

View File

@ -26,7 +26,6 @@ public sealed class SortableFieldsAttributeFilter : IParameterFilter
var description = string.Join("<br/>", attribute.GetFields().Order());
parameter.Description = $"{parameter.Description}<br/><br/>" +
$"**Allowed values:**<br/>{description}";
$"**Allowed values:**<br/>{description}";
}
}
}

View File

@ -6,4 +6,4 @@ namespace CleanArchitecture.Api.Swagger;
public abstract class SwaggerSortableFieldsAttribute : Attribute
{
public abstract IEnumerable<string> GetFields();
}
}

View File

@ -8,12 +8,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="MockQueryable.NSubstitute" Version="7.0.0"/>
<PackageReference Include="NSubstitute" Version="5.0.0"/>
<PackageReference Include="xunit" Version="2.5.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="MockQueryable.NSubstitute" Version="7.0.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -24,8 +24,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -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<Guid>(y => y == tenant.Id)).Returns(tenant);
}
var tenantList = new List<Tenant> { tenant }.BuildMock();
TenantRepository.GetAllNoTracking().Returns(tenantList);
return tenant;
}

View File

@ -35,7 +35,7 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
var query = new[] { user }.BuildMock();
UserRepository.GetAllNoTracking().Returns(query);
UserRepository.GetByIdAsync(Arg.Is<Guid>(y => y == ExistingUserId)).Returns(user);
}
public void SetupDeletedUserAsync()

View File

@ -6,12 +6,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.13" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj" />
</ItemGroup>

View File

@ -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))

View File

@ -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<TenantViewModel?> 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)
{

View File

@ -32,6 +32,7 @@ public sealed class GetAllUsersQueryHandler :
{
var usersQuery = _userRepository
.GetAllNoTracking()
.IgnoreQueryFilters()
.Where(x => request.IncludeDeleted || !x.Deleted);
if (!string.IsNullOrWhiteSpace(request.SearchTerm))

View File

@ -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<UserViewModel?> 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)
{

View File

@ -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
{

View File

@ -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
{

View File

@ -8,12 +8,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3"/>
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="NSubstitute" Version="5.0.0"/>
<PackageReference Include="xunit" Version="2.5.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -24,7 +24,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -6,20 +6,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3"/>
<PackageReference Include="FluentValidation" Version="11.7.1"/>
<PackageReference Include="MediatR" Version="12.1.1"/>
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="RabbitMQ.Client" Version="6.5.0"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.1"/>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FluentValidation" Version="11.8.0" />
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RabbitMQ.Client" Version="6.6.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Shared\CleanArchitecture.Shared.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Shared\CleanArchitecture.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -20,7 +20,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
public sealed class LoginUserCommandHandler : CommandHandlerBase,
IRequestHandler<LoginUserCommand, string>
{
private const double _expiryDurationMinutes = 30;
private const double _expiryDurationMinutes = 60;
private readonly TokenSettings _tokenSettings;
private readonly IUserRepository _userRepository;

View File

@ -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;
}

View File

@ -8,11 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="NSubstitute" Version="5.0.0"/>
<PackageReference Include="xunit" Version="2.5.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -6,20 +6,20 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Shared\CleanArchitecture.Shared.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.Shared\CleanArchitecture.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.1.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.10">
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@ -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<User> Users { get; set; } = null!;
public DbSet<Tenant> 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());

View File

@ -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;
}
}
}

View File

@ -0,0 +1,138 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("Deleted")
.HasColumnType("bit");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("Deleted")
.HasColumnType("bit");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(320)
.HasColumnType("nvarchar(320)");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset?>("LastLoggedinDate")
.HasColumnType("datetimeoffset");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<int>("Role")
.HasColumnType("int");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<Guid>("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
}
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CleanArchitecture.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class DeletedQueryFilter : Migration
{
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
}
}

View File

@ -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");

View File

@ -8,15 +8,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10"/>
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="xunit" Version="2.5.0"/>
<PackageReference Include="Xunit.Priority" Version="1.1.6"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.13" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="xunit" Version="2.6.0" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -27,8 +27,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Api\CleanArchitecture.Api.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Api\CleanArchitecture.Api.csproj" />
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup>

View File

@ -37,6 +37,8 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
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<TenantTestFixture>
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]

View File

@ -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();
}
}

View File

@ -6,16 +6,16 @@
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Users\Models.proto" GrpcServices="Both"/>
<Protobuf Include="Users\UsersApi.proto" GrpcServices="Both"/>
<Protobuf Include="Tenants\Models.proto" GrpcServices="Both"/>
<Protobuf Include="Tenants\TenantsApi.proto" GrpcServices="Both"/>
<Protobuf Include="Users\Models.proto" GrpcServices="Both" />
<Protobuf Include="Users\UsersApi.proto" GrpcServices="Both" />
<Protobuf Include="Tenants\Models.proto" GrpcServices="Both" />
<Protobuf Include="Tenants\TenantsApi.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.24.2"/>
<PackageReference Include="Google.Protobuf.Tools" Version="3.24.2"/>
<PackageReference Include="Grpc.AspNetCore" Version="2.56.0"/>
<PackageReference Include="Google.Protobuf" Version="3.24.4" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.24.4" />
<PackageReference Include="Grpc.AspNetCore" Version="2.58.0" />
</ItemGroup>
</Project>

View File

@ -8,12 +8,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="MockQueryable.NSubstitute" Version="7.0.0"/>
<PackageReference Include="NSubstitute" Version="5.0.0"/>
<PackageReference Include="xunit" Version="2.5.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="MockQueryable.NSubstitute" Version="7.0.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -24,9 +24,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj"/>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" />
</ItemGroup>
</Project>

View File

@ -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

View File

@ -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

View File

@ -31,4 +31,7 @@ spec:
# Replace this with the path to your built image
image: alexdev28/clean-architecture:latest
ports:
- containerPort: 80
- 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!#