0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +00:00

feat: Deleted query filter

This commit is contained in:
alex289 2023-10-01 14:54:49 +02:00
parent bfe40e180e
commit 4b2619f13c
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
20 changed files with 96 additions and 30 deletions

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

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

@ -32,6 +32,7 @@ public sealed class GetAllTenantsQueryHandler :
{
var tenantsQuery = _tenantRepository
.GetAllNoTracking()
.IgnoreQueryFilters()
.Include(x => x.Users)
.Where(x => request.IncludeDeleted || !x.Deleted);

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

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

View File

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

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

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

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