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

Merge pull request #21 from alex289/feature/pagination

feat: Add pagination
This commit is contained in:
Alex 2023-08-31 19:35:46 +02:00 committed by GitHub
commit f46994c60a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 204 additions and 52 deletions

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CleanArchitecture.Api.Models;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Domain.Notifications;
using MediatR;
@ -28,10 +28,14 @@ public sealed class TenantController : ApiController
[HttpGet]
[SwaggerOperation("Get a list of all tenants")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<TenantViewModel>>))]
public async Task<IActionResult> GetAllTenantsAsync()
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<TenantViewModel>>))]
public async Task<IActionResult> GetAllTenantsAsync(
[FromQuery] PageQuery query,
[FromQuery] string searchTerm = "")
{
var tenants = await _tenantService.GetAllTenantsAsync();
var tenants = await _tenantService.GetAllTenantsAsync(
query,
searchTerm);
return Response(tenants);
}

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CleanArchitecture.Api.Models;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Notifications;
using MediatR;
@ -28,10 +28,14 @@ public sealed class UserController : ApiController
[HttpGet]
[SwaggerOperation("Get a list of all users")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))]
public async Task<IActionResult> GetAllUsersAsync()
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<UserViewModel>>))]
public async Task<IActionResult> GetAllUsersAsync(
[FromQuery] PageQuery query,
[FromQuery] string searchTerm = "")
{
var users = await _userService.GetAllUsersAsync();
var users = await _userService.GetAllUsersAsync(
query,
searchTerm);
return Response(users);
}

View File

@ -21,7 +21,7 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
Handler = new GetAllUsersQueryHandler(UserRepository);
}
public void SetupUserAsync()
public User SetupUserAsync()
{
var user = new User(
ExistingUserId,
@ -35,6 +35,8 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
var query = new[] { user }.BuildMock();
UserRepository.GetAllNoTracking().Returns(query);
return user;
}
public void SetupDeletedUserAsync()

View File

@ -2,6 +2,7 @@ using System.Linq;
using System.Threading.Tasks;
using CleanArchitecture.Application.Queries.Tenants.GetAll;
using CleanArchitecture.Application.Tests.Fixtures.Queries.Tenants;
using CleanArchitecture.Application.ViewModels;
using FluentAssertions;
using Xunit;
@ -16,24 +17,44 @@ public sealed class GetAllTenantsQueryHandlerTests
{
var tenant = _fixture.SetupTenant();
var query = new PageQuery
{
PageSize = 10,
Page = 1
};
var result = await _fixture.QueryHandler.Handle(
new GetAllTenantsQuery(),
new GetAllTenantsQuery(query),
default);
_fixture.VerifyNoDomainNotification();
tenant.Should().BeEquivalentTo(result.First());
result.PageSize.Should().Be(query.PageSize);
result.Page.Should().Be(query.Page);
result.Count.Should().Be(1);
tenant.Should().BeEquivalentTo(result.Items.First());
}
[Fact]
public async Task Should_Not_Get_Deleted_Tenant()
{
_fixture.SetupTenant(true);
var query = new PageQuery
{
PageSize = 10,
Page = 1
};
var result = await _fixture.QueryHandler.Handle(
new GetAllTenantsQuery(),
new GetAllTenantsQuery(query),
default);
result.PageSize.Should().Be(query.PageSize);
result.Page.Should().Be(query.Page);
result.Count.Should().Be(0);
result.Should().HaveCount(0);
result.Items.Should().HaveCount(0);
}
}

View File

@ -2,6 +2,7 @@ using System.Linq;
using System.Threading.Tasks;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
using CleanArchitecture.Application.ViewModels;
using FluentAssertions;
using Xunit;
@ -14,15 +15,25 @@ public sealed class GetAllUsersQueryHandlerTests
[Fact]
public async Task Should_Get_All_Users()
{
_fixture.SetupUserAsync();
var user = _fixture.SetupUserAsync();
var query = new PageQuery
{
PageSize = 1,
Page = 1
};
var result = await _fixture.Handler.Handle(
new GetAllUsersQuery(),
new GetAllUsersQuery(query, user.Email),
default);
_fixture.VerifyNoDomainNotification();
result.PageSize.Should().Be(query.PageSize);
result.Page.Should().Be(query.Page);
result.Count.Should().Be(1);
var userViewModels = result.ToArray();
var userViewModels = result.Items.ToArray();
userViewModels.Should().NotBeNull();
userViewModels.Should().ContainSingle();
userViewModels.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId);
@ -32,13 +43,23 @@ public sealed class GetAllUsersQueryHandlerTests
public async Task Should_Not_Get_Deleted_Users()
{
_fixture.SetupDeletedUserAsync();
var query = new PageQuery
{
PageSize = 10,
Page = 1
};
var result = await _fixture.Handler.Handle(
new GetAllUsersQuery(),
new GetAllUsersQuery(query),
default);
_fixture.VerifyNoDomainNotification();
result.PageSize.Should().Be(query.PageSize);
result.Page.Should().Be(query.Page);
result.Count.Should().Be(0);
result.Should().BeEmpty();
result.Items.Should().BeEmpty();
}
}

View File

@ -1,10 +1,10 @@
using System.Collections.Generic;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.Queries.Tenants.GetAll;
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.Services;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
@ -26,12 +26,12 @@ public static class ServiceCollectionExtension
{
// User
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
services.AddScoped<IRequestHandler<GetAllUsersQuery, PagedResult<UserViewModel>>, GetAllUsersQueryHandler>();
// Tenant
services.AddScoped<IRequestHandler<GetTenantByIdQuery, TenantViewModel?>, GetTenantByIdQueryHandler>();
services
.AddScoped<IRequestHandler<GetAllTenantsQuery, IEnumerable<TenantViewModel>>, GetAllTenantsQueryHandler>();
.AddScoped<IRequestHandler<GetAllTenantsQuery, PagedResult<TenantViewModel>>, GetAllTenantsQueryHandler>();
return services;
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
namespace CleanArchitecture.Application.Interfaces;
@ -11,5 +12,5 @@ public interface ITenantService
public Task UpdateTenantAsync(UpdateTenantViewModel tenant);
public Task DeleteTenantAsync(Guid tenantId);
public Task<TenantViewModel?> GetTenantByIdAsync(Guid tenantId, bool deleted);
public Task<IEnumerable<TenantViewModel>> GetAllTenantsAsync();
public Task<PagedResult<TenantViewModel>> GetAllTenantsAsync(PageQuery query, string searchTerm = "");
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
namespace CleanArchitecture.Application.Interfaces;
@ -9,7 +10,7 @@ public interface IUserService
{
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted);
public Task<UserViewModel?> GetCurrentUserAsync();
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
public Task<PagedResult<UserViewModel>> GetAllUsersAsync(PageQuery query, string searchTerm = "");
public Task<Guid> CreateUserAsync(CreateUserViewModel user);
public Task UpdateUserAsync(UpdateUserViewModel user);
public Task DeleteUserAsync(Guid userId);

View File

@ -1,7 +1,8 @@
using System.Collections.Generic;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
using MediatR;
namespace CleanArchitecture.Application.Queries.Tenants.GetAll;
public sealed record GetAllTenantsQuery : IRequest<IEnumerable<TenantViewModel>>;
public sealed record GetAllTenantsQuery(PageQuery Query, string SearchTerm = "") :
IRequest<PagedResult<TenantViewModel>>;

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MediatR;
@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore;
namespace CleanArchitecture.Application.Queries.Tenants.GetAll;
public sealed class GetAllTenantsQueryHandler :
IRequestHandler<GetAllTenantsQuery, IEnumerable<TenantViewModel>>
IRequestHandler<GetAllTenantsQuery, PagedResult<TenantViewModel>>
{
private readonly ITenantRepository _tenantRepository;
@ -19,15 +19,30 @@ public sealed class GetAllTenantsQueryHandler :
_tenantRepository = tenantRepository;
}
public async Task<IEnumerable<TenantViewModel>> Handle(
public async Task<PagedResult<TenantViewModel>> Handle(
GetAllTenantsQuery request,
CancellationToken cancellationToken)
{
return await _tenantRepository
var tenantsQuery = _tenantRepository
.GetAllNoTracking()
.Include(x => x.Users)
.Where(x => !x.Deleted)
.Select(x => TenantViewModel.FromTenant(x))
.Where(x => !x.Deleted);
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
{
tenantsQuery = tenantsQuery.Where(tenant =>
tenant.Name.Contains(request.SearchTerm));
}
var totalCount = await tenantsQuery.CountAsync(cancellationToken);
var tenants = await tenantsQuery
.Skip((request.Query.Page - 1) * request.Query.PageSize)
.Take(request.Query.PageSize)
.Select(tenant => TenantViewModel.FromTenant(tenant))
.ToListAsync(cancellationToken);
return new PagedResult<TenantViewModel>(
totalCount, tenants, request.Query.Page, request.Query.PageSize);
}
}

View File

@ -1,7 +1,8 @@
using System.Collections.Generic;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetAll;
public sealed record GetAllUsersQuery : IRequest<IEnumerable<UserViewModel>>;
public sealed record GetAllUsersQuery(PageQuery Query, string SearchTerm = "") :
IRequest<PagedResult<UserViewModel>>;

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MediatR;
@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore;
namespace CleanArchitecture.Application.Queries.Users.GetAll;
public sealed class GetAllUsersQueryHandler :
IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>
IRequestHandler<GetAllUsersQuery, PagedResult<UserViewModel>>
{
private readonly IUserRepository _userRepository;
@ -19,12 +19,31 @@ public sealed class GetAllUsersQueryHandler :
_userRepository = userRepository;
}
public async Task<IEnumerable<UserViewModel>> Handle(GetAllUsersQuery request, CancellationToken cancellationToken)
public async Task<PagedResult<UserViewModel>> Handle(
GetAllUsersQuery request,
CancellationToken cancellationToken)
{
return await _userRepository
var usersQuery = _userRepository
.GetAllNoTracking()
.Where(x => !x.Deleted)
.Select(x => UserViewModel.FromUser(x))
.Where(x => !x.Deleted);
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
{
usersQuery = usersQuery.Where(user =>
user.Email.Contains(request.SearchTerm) ||
user.FirstName.Contains(request.SearchTerm) ||
user.LastName.Contains(request.SearchTerm));
}
var totalCount = await usersQuery.CountAsync(cancellationToken);
var users = await usersQuery
.Skip((request.Query.Page - 1) * request.Query.PageSize)
.Take(request.Query.PageSize)
.Select(user => UserViewModel.FromUser(user))
.ToListAsync(cancellationToken);
return new PagedResult<UserViewModel>(
totalCount, users, request.Query.Page, request.Query.PageSize);
}
}

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.Queries.Tenants.GetAll;
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Domain.Commands.Tenants.CreateTenant;
using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant;
@ -49,8 +50,8 @@ public sealed class TenantService : ITenantService
return await _bus.QueryAsync(new GetTenantByIdQuery(tenantId, deleted));
}
public async Task<IEnumerable<TenantViewModel>> GetAllTenantsAsync()
public async Task<PagedResult<TenantViewModel>> GetAllTenantsAsync(PageQuery query, string searchTerm = "")
{
return await _bus.QueryAsync(new GetAllTenantsQuery());
return await _bus.QueryAsync(new GetAllTenantsQuery(query, searchTerm));
}
}

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
@ -35,9 +36,9 @@ public sealed class UserService : IUserService
return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false));
}
public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()
public async Task<PagedResult<UserViewModel>> GetAllUsersAsync(PageQuery query, string searchTerm = "")
{
return await _bus.QueryAsync(new GetAllUsersQuery());
return await _bus.QueryAsync(new GetAllUsersQuery(query, searchTerm));
}
public async Task<Guid> CreateUserAsync(CreateUserViewModel user)

View File

@ -0,0 +1,20 @@
using System;
namespace CleanArchitecture.Application.ViewModels;
public sealed class PageQuery
{
private int _pageSize = 10;
public int PageSize
{
get => _pageSize;
set => _pageSize = Math.Max(0, value);
}
private int _page = 1;
public int Page
{
get => _page;
set => _page = Math.Max(1, value);
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
namespace CleanArchitecture.Application.ViewModels;
public sealed class PagedResult<T>
{
public int Count { get; init; }
public IList<T> Items { get; init; } = Array.Empty<T>();
public int Page { get; init; }
public int PageSize { get; init; }
public PagedResult(int count, IList<T> items, int page, int pageSize)
{
Count = count;
Items = items;
Page = page;
PageSize = pageSize;
}
// used by json deserializer
private PagedResult()
{
}
public static PagedResult<T> Empty()
{
return new PagedResult<T>
{
Count = 0,
Items = Array.Empty<T>(),
Page = 1,
PageSize = 10
};
}
}

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures;
@ -43,15 +43,16 @@ public sealed class TenantControllerTests : IClassFixture<TenantTestFixture>
[Priority(5)]
public async Task Should_Get_All_Tenants()
{
var response = await _fixture.ServerClient.GetAsync("api/v1/Tenant");
var response = await _fixture.ServerClient.GetAsync(
"api/v1/Tenant?searchTerm=Test&pageSize=5&page=1");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<TenantViewModel>>();
var message = await response.Content.ReadAsJsonAsync<PagedResult<TenantViewModel>>();
message?.Data.Should().NotBeEmpty();
message!.Data.Should().HaveCountGreaterOrEqualTo(2);
message.Data!
message?.Data!.Items.Should().NotBeEmpty();
message!.Data!.Items.Should().HaveCount(1);
message.Data!.Items
.FirstOrDefault(x => x.Id == _fixture.CreatedTenantId)
.Should().NotBeNull();
}

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Enums;
@ -34,11 +34,11 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<UserViewModel>>();
var message = await response.Content.ReadAsJsonAsync<PagedResult<UserViewModel>>();
message?.Data.Should().NotBeNull();
var content = message!.Data!.ToList();
var content = message!.Data!.Items.ToList();
content.Count.Should().Be(2);