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/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/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs index a097c13..afa3d0a 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -32,6 +32,7 @@ public sealed class GetAllTenantsQueryHandler : { var tenantsQuery = _tenantRepository .GetAllNoTracking() + .IgnoreQueryFilters() .Include(x => x.Users) .Where(x => request.IncludeDeleted || !x.Deleted); diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs index 652c458..4473ae0 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,8 @@ 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/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs index 979831b..b44cb19 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/Tenant/CreateTenant/CreateTenantCommandHandlerTests.cs @@ -12,7 +12,7 @@ public sealed class CreateTenantCommandHandlerTests private readonly CreateTenantCommandTestFixture _fixture = new(); [Fact] - public async Task Should_Create_Tenant() + public async Task Should_Create_Tenant() { var command = new CreateTenantCommand( Guid.NewGuid(), @@ -29,7 +29,7 @@ public sealed class CreateTenantCommandHandlerTests } [Fact] - public async Task Should_Not_Create_Tenant_Insufficient_Permissions() + public async Task Should_Not_Create_Tenant_Insufficient_Permissions() { _fixture.SetupUser(); @@ -49,7 +49,7 @@ public sealed class CreateTenantCommandHandlerTests } [Fact] - public async Task Should_Not_Create_Tenant_Already_Exists() + public async Task Should_Not_Create_Tenant_Already_Exists() { var command = new CreateTenantCommand( Guid.NewGuid(), diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs index 19344e8..c61ddb6 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -27,7 +27,7 @@ public sealed class DeleteUserCommandHandlerTests } [Fact] - public async Task Should_Not_Delete_Non_Existing_User() + public async Task Should_Not_Delete_Non_Existing_User() { _fixture.SetupUser(); @@ -45,7 +45,7 @@ public sealed class DeleteUserCommandHandlerTests } [Fact] - public async Task Should_Not_Delete_User_Insufficient_Permissions() + public async Task Should_Not_Delete_User_Insufficient_Permissions() { var user = _fixture.SetupUser(); 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/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.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