mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-07-01 11:02:57 +00:00
feat: Deleted query filter
This commit is contained in:
parent
bfe40e180e
commit
4b2619f13c
@ -37,7 +37,8 @@ public sealed class TenantController : ApiController
|
|||||||
[FromQuery] PageQuery query,
|
[FromQuery] PageQuery query,
|
||||||
[FromQuery] string searchTerm = "",
|
[FromQuery] string searchTerm = "",
|
||||||
[FromQuery] bool includeDeleted = false,
|
[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(
|
var tenants = await _tenantService.GetAllTenantsAsync(
|
||||||
query,
|
query,
|
||||||
|
@ -37,7 +37,8 @@ public sealed class UserController : ApiController
|
|||||||
[FromQuery] PageQuery query,
|
[FromQuery] PageQuery query,
|
||||||
[FromQuery] string searchTerm = "",
|
[FromQuery] string searchTerm = "",
|
||||||
[FromQuery] bool includeDeleted = false,
|
[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(
|
var users = await _userService.GetAllUsersAsync(
|
||||||
query,
|
query,
|
||||||
|
@ -4,7 +4,7 @@ using CleanArchitecture.Application.ViewModels.Sorting;
|
|||||||
|
|
||||||
namespace CleanArchitecture.Api.Swagger;
|
namespace CleanArchitecture.Api.Swagger;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Parameter)]
|
||||||
public sealed class SortableFieldsAttribute<TSortingProvider, TViewModel, TEntity>
|
public sealed class SortableFieldsAttribute<TSortingProvider, TViewModel, TEntity>
|
||||||
: SwaggerSortableFieldsAttribute
|
: SwaggerSortableFieldsAttribute
|
||||||
where TSortingProvider : ISortingExpressionProvider<TViewModel, TEntity>, new()
|
where TSortingProvider : ISortingExpressionProvider<TViewModel, TEntity>, new()
|
||||||
|
@ -26,7 +26,6 @@ public sealed class SortableFieldsAttributeFilter : IParameterFilter
|
|||||||
var description = string.Join("<br/>", attribute.GetFields().Order());
|
var description = string.Join("<br/>", attribute.GetFields().Order());
|
||||||
|
|
||||||
parameter.Description = $"{parameter.Description}<br/><br/>" +
|
parameter.Description = $"{parameter.Description}<br/><br/>" +
|
||||||
$"**Allowed values:**<br/>{description}";
|
$"**Allowed values:**<br/>{description}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,4 +6,4 @@ namespace CleanArchitecture.Api.Swagger;
|
|||||||
public abstract class SwaggerSortableFieldsAttribute : Attribute
|
public abstract class SwaggerSortableFieldsAttribute : Attribute
|
||||||
{
|
{
|
||||||
public abstract IEnumerable<string> GetFields();
|
public abstract IEnumerable<string> GetFields();
|
||||||
}
|
}
|
@ -1,9 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
||||||
using CleanArchitecture.Domain.Entities;
|
using CleanArchitecture.Domain.Entities;
|
||||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
using MockQueryable.NSubstitute;
|
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants;
|
namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants;
|
||||||
@ -30,9 +28,11 @@ public sealed class GetTenantByIdTestFixture : QueryHandlerBaseFixture
|
|||||||
{
|
{
|
||||||
tenant.Delete();
|
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;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
|
|||||||
|
|
||||||
var query = new[] { user }.BuildMock();
|
var query = new[] { user }.BuildMock();
|
||||||
|
|
||||||
UserRepository.GetAllNoTracking().Returns(query);
|
UserRepository.GetByIdAsync(Arg.Is<Guid>(y => y == ExistingUserId)).Returns(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetupDeletedUserAsync()
|
public void SetupDeletedUserAsync()
|
||||||
|
@ -32,6 +32,7 @@ public sealed class GetAllTenantsQueryHandler :
|
|||||||
{
|
{
|
||||||
var tenantsQuery = _tenantRepository
|
var tenantsQuery = _tenantRepository
|
||||||
.GetAllNoTracking()
|
.GetAllNoTracking()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
.Include(x => x.Users)
|
.Include(x => x.Users)
|
||||||
.Where(x => request.IncludeDeleted || !x.Deleted);
|
.Where(x => request.IncludeDeleted || !x.Deleted);
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
@ -7,7 +6,6 @@ using CleanArchitecture.Domain.Interfaces;
|
|||||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
using CleanArchitecture.Domain.Notifications;
|
using CleanArchitecture.Domain.Notifications;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
||||||
|
|
||||||
@ -25,10 +23,8 @@ public sealed class GetTenantByIdQueryHandler :
|
|||||||
|
|
||||||
public async Task<TenantViewModel?> Handle(GetTenantByIdQuery request, CancellationToken cancellationToken)
|
public async Task<TenantViewModel?> Handle(GetTenantByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var tenant = _tenantRepository
|
var tenant = await _tenantRepository
|
||||||
.GetAllNoTracking()
|
.GetByIdAsync(request.TenantId);
|
||||||
.Include(x => x.Users)
|
|
||||||
.FirstOrDefault(x => x.Id == request.TenantId && !x.Deleted);
|
|
||||||
|
|
||||||
if (tenant is null)
|
if (tenant is null)
|
||||||
{
|
{
|
||||||
|
@ -32,6 +32,7 @@ public sealed class GetAllUsersQueryHandler :
|
|||||||
{
|
{
|
||||||
var usersQuery = _userRepository
|
var usersQuery = _userRepository
|
||||||
.GetAllNoTracking()
|
.GetAllNoTracking()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
.Where(x => request.IncludeDeleted || !x.Deleted);
|
.Where(x => request.IncludeDeleted || !x.Deleted);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Threading;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
using CleanArchitecture.Domain.Errors;
|
using CleanArchitecture.Domain.Errors;
|
||||||
@ -24,9 +23,7 @@ public sealed class GetUserByIdQueryHandler :
|
|||||||
|
|
||||||
public async Task<UserViewModel?> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
public async Task<UserViewModel?> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var user = _userRepository
|
var user = await _userRepository.GetByIdAsync(request.UserId);
|
||||||
.GetAllNoTracking()
|
|
||||||
.FirstOrDefault(x => x.Id == request.UserId && !x.Deleted);
|
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
|
@ -34,6 +34,7 @@ public sealed class TenantsApiImplementation : TenantsApi.TenantsApiBase
|
|||||||
|
|
||||||
var tenants = await _tenantRepository
|
var tenants = await _tenantRepository
|
||||||
.GetAllNoTracking()
|
.GetAllNoTracking()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
.Where(tenant => idsAsGuids.Contains(tenant.Id))
|
.Where(tenant => idsAsGuids.Contains(tenant.Id))
|
||||||
.Select(tenant => new Tenant
|
.Select(tenant => new Tenant
|
||||||
{
|
{
|
||||||
|
@ -34,6 +34,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase
|
|||||||
|
|
||||||
var users = await _userRepository
|
var users = await _userRepository
|
||||||
.GetAllNoTracking()
|
.GetAllNoTracking()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
.Where(user => idsAsGuids.Contains(user.Id))
|
.Where(user => idsAsGuids.Contains(user.Id))
|
||||||
.Select(user => new GrpcUser
|
.Select(user => new GrpcUser
|
||||||
{
|
{
|
||||||
|
@ -12,7 +12,7 @@ public sealed class CreateTenantCommandHandlerTests
|
|||||||
private readonly CreateTenantCommandTestFixture _fixture = new();
|
private readonly CreateTenantCommandTestFixture _fixture = new();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Should_Create_Tenant()
|
public async Task Should_Create_Tenant()
|
||||||
{
|
{
|
||||||
var command = new CreateTenantCommand(
|
var command = new CreateTenantCommand(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
@ -29,7 +29,7 @@ public sealed class CreateTenantCommandHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Should_Not_Create_Tenant_Insufficient_Permissions()
|
public async Task Should_Not_Create_Tenant_Insufficient_Permissions()
|
||||||
{
|
{
|
||||||
_fixture.SetupUser();
|
_fixture.SetupUser();
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ public sealed class CreateTenantCommandHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Should_Not_Create_Tenant_Already_Exists()
|
public async Task Should_Not_Create_Tenant_Already_Exists()
|
||||||
{
|
{
|
||||||
var command = new CreateTenantCommand(
|
var command = new CreateTenantCommand(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
@ -27,7 +27,7 @@ public sealed class DeleteUserCommandHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Should_Not_Delete_Non_Existing_User()
|
public async Task Should_Not_Delete_Non_Existing_User()
|
||||||
{
|
{
|
||||||
_fixture.SetupUser();
|
_fixture.SetupUser();
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ public sealed class DeleteUserCommandHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Should_Not_Delete_User_Insufficient_Permissions()
|
public async Task Should_Not_Delete_User_Insufficient_Permissions()
|
||||||
{
|
{
|
||||||
var user = _fixture.SetupUser();
|
var user = _fixture.SetupUser();
|
||||||
|
|
||||||
|
@ -52,7 +52,8 @@ public sealed class RabbitMqHandler : BackgroundService
|
|||||||
{
|
{
|
||||||
if (!_configuration.Enabled)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
using System.Linq;
|
||||||
using CleanArchitecture.Domain.Entities;
|
using CleanArchitecture.Domain.Entities;
|
||||||
using CleanArchitecture.Infrastructure.Configurations;
|
using CleanArchitecture.Infrastructure.Configurations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace CleanArchitecture.Infrastructure.Database;
|
namespace CleanArchitecture.Infrastructure.Database;
|
||||||
|
|
||||||
public class ApplicationDbContext : DbContext
|
public partial class ApplicationDbContext : DbContext
|
||||||
{
|
{
|
||||||
public DbSet<User> Users { get; set; } = null!;
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
public DbSet<Tenant> Tenants { get; set; } = null!;
|
public DbSet<Tenant> Tenants { get; set; } = null!;
|
||||||
@ -14,6 +15,29 @@ public class ApplicationDbContext : DbContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
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 UserConfiguration());
|
||||||
builder.ApplyConfiguration(new TenantConfiguration());
|
builder.ApplyConfiguration(new TenantConfiguration());
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,8 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
|
|||||||
|
|
||||||
message!.Data!.Id.Should().Be(_fixture.CreatedTenantId);
|
message!.Data!.Id.Should().Be(_fixture.CreatedTenantId);
|
||||||
message.Data.Name.Should().Be("Test Tenant");
|
message.Data.Name.Should().Be("Test Tenant");
|
||||||
|
|
||||||
|
message.Data.Users.Count().Should().Be(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -55,6 +57,10 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
|
|||||||
message.Data!.Items
|
message.Data!.Items
|
||||||
.FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)
|
.FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)
|
||||||
.Should().NotBeNull();
|
.Should().NotBeNull();
|
||||||
|
|
||||||
|
message.Data.Items
|
||||||
|
.FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)!
|
||||||
|
.Users.Count().Should().Be(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using CleanArchitecture.Domain.Entities;
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
using CleanArchitecture.Domain.Enums;
|
||||||
using CleanArchitecture.Infrastructure.Database;
|
using CleanArchitecture.Infrastructure.Database;
|
||||||
|
|
||||||
namespace CleanArchitecture.IntegrationTests.Fixtures;
|
namespace CleanArchitecture.IntegrationTests.Fixtures;
|
||||||
@ -16,6 +17,15 @@ public sealed class TenantTestFixture : TestFixtureBase
|
|||||||
CreatedTenantId,
|
CreatedTenantId,
|
||||||
"Test Tenant"));
|
"Test Tenant"));
|
||||||
|
|
||||||
|
context.Users.Add(new User(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
CreatedTenantId,
|
||||||
|
"test@user.de",
|
||||||
|
"test",
|
||||||
|
"user",
|
||||||
|
"Test User",
|
||||||
|
UserRole.User));
|
||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user