mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-06-30 02:31:08 +00:00
Add sorting
This commit is contained in:
parent
09c21f23a3
commit
b54d4f4de5
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using CleanArchitecture.Api.Models;
|
||||
using CleanArchitecture.Application.Interfaces;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using CleanArchitecture.Domain.Notifications;
|
||||
using MediatR;
|
||||
@ -31,11 +32,15 @@ public sealed class TenantController : ApiController
|
||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<TenantViewModel>>))]
|
||||
public async Task<IActionResult> GetAllTenantsAsync(
|
||||
[FromQuery] PageQuery query,
|
||||
[FromQuery] string searchTerm = "")
|
||||
[FromQuery] string searchTerm = "",
|
||||
[FromQuery] bool includeDeleted = false,
|
||||
[FromQuery] SortQuery? sortQuery = null)
|
||||
{
|
||||
var tenants = await _tenantService.GetAllTenantsAsync(
|
||||
query,
|
||||
searchTerm);
|
||||
includeDeleted,
|
||||
searchTerm,
|
||||
sortQuery);
|
||||
return Response(tenants);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using CleanArchitecture.Api.Models;
|
||||
using CleanArchitecture.Application.Interfaces;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Domain.Notifications;
|
||||
using MediatR;
|
||||
@ -31,11 +32,15 @@ public sealed class UserController : ApiController
|
||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<UserViewModel>>))]
|
||||
public async Task<IActionResult> GetAllUsersAsync(
|
||||
[FromQuery] PageQuery query,
|
||||
[FromQuery] string searchTerm = "")
|
||||
[FromQuery] string searchTerm = "",
|
||||
[FromQuery] bool includeDeleted = false,
|
||||
[FromQuery] SortQuery? sortQuery = null)
|
||||
{
|
||||
var users = await _userService.GetAllUsersAsync(
|
||||
query,
|
||||
searchTerm);
|
||||
includeDeleted,
|
||||
searchTerm,
|
||||
sortQuery);
|
||||
return Response(users);
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,7 @@ builder.Services.AddAuth(builder.Configuration);
|
||||
builder.Services.AddInfrastructure(builder.Configuration, "CleanArchitecture.Infrastructure");
|
||||
builder.Services.AddQueryHandlers();
|
||||
builder.Services.AddServices();
|
||||
builder.Services.AddSortProviders();
|
||||
builder.Services.AddCommandHandlers();
|
||||
builder.Services.AddNotificationHandlers();
|
||||
builder.Services.AddApiUser();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
||||
using CleanArchitecture.Application.SortProviders;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using MockQueryable.NSubstitute;
|
||||
@ -16,8 +17,9 @@ public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture
|
||||
public GetAllTenantsTestFixture()
|
||||
{
|
||||
TenantRepository = Substitute.For<ITenantRepository>();
|
||||
var sortingProvider = new TenantViewModelSortProvider();
|
||||
|
||||
QueryHandler = new GetAllTenantsQueryHandler(TenantRepository);
|
||||
QueryHandler = new GetAllTenantsQueryHandler(TenantRepository, sortingProvider);
|
||||
}
|
||||
|
||||
public Tenant SetupTenant(bool deleted = false)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using CleanArchitecture.Application.Queries.Users.GetAll;
|
||||
using CleanArchitecture.Application.SortProviders;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
@ -17,8 +18,9 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
|
||||
public GetAllUsersTestFixture()
|
||||
{
|
||||
UserRepository = Substitute.For<IUserRepository>();
|
||||
var sortingProvider = new UserViewModelSortProvider();
|
||||
|
||||
Handler = new GetAllUsersQueryHandler(UserRepository);
|
||||
Handler = new GetAllUsersQueryHandler(UserRepository, sortingProvider);
|
||||
}
|
||||
|
||||
public User SetupUserAsync()
|
||||
|
@ -24,7 +24,7 @@ public sealed class GetAllTenantsQueryHandlerTests
|
||||
};
|
||||
|
||||
var result = await _fixture.QueryHandler.Handle(
|
||||
new GetAllTenantsQuery(query),
|
||||
new GetAllTenantsQuery(query, false),
|
||||
default);
|
||||
|
||||
_fixture.VerifyNoDomainNotification();
|
||||
@ -48,7 +48,7 @@ public sealed class GetAllTenantsQueryHandlerTests
|
||||
};
|
||||
|
||||
var result = await _fixture.QueryHandler.Handle(
|
||||
new GetAllTenantsQuery(query),
|
||||
new GetAllTenantsQuery(query, false),
|
||||
default);
|
||||
|
||||
result.PageSize.Should().Be(query.PageSize);
|
||||
|
@ -24,7 +24,7 @@ public sealed class GetAllUsersQueryHandlerTests
|
||||
};
|
||||
|
||||
var result = await _fixture.Handler.Handle(
|
||||
new GetAllUsersQuery(query, user.Email),
|
||||
new GetAllUsersQuery(query, false, user.Email),
|
||||
default);
|
||||
|
||||
_fixture.VerifyNoDomainNotification();
|
||||
@ -51,7 +51,7 @@ public sealed class GetAllUsersQueryHandlerTests
|
||||
};
|
||||
|
||||
var result = await _fixture.Handler.Handle(
|
||||
new GetAllUsersQuery(query),
|
||||
new GetAllUsersQuery(query, false),
|
||||
default);
|
||||
|
||||
_fixture.VerifyNoDomainNotification();
|
||||
|
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
|
||||
namespace CleanArchitecture.Application.Extensions;
|
||||
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IQueryable<TEntity> GetOrderedQueryable<TEntity, TViewModel>(
|
||||
this IQueryable<TEntity> query,
|
||||
SortQuery? sort,
|
||||
ISortingExpressionProvider<TViewModel, TEntity> expressionProvider)
|
||||
{
|
||||
return GetOrderedQueryable(query, sort, expressionProvider.GetSortingExpressions());
|
||||
}
|
||||
|
||||
public static IQueryable<TEntity> GetOrderedQueryable<TEntity>(
|
||||
this IQueryable<TEntity> query,
|
||||
SortQuery? sort,
|
||||
Dictionary<string, Expression<Func<TEntity, object>>> fieldExpressions)
|
||||
{
|
||||
if (sort is null || !sort.Parameters.Any())
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
var sorted = GetFirstOrderLevelQuery(query, sort.Parameters.First(), fieldExpressions);
|
||||
|
||||
for (int i = 1; i < sort.Parameters.Count; i++)
|
||||
{
|
||||
sorted = GetMultiLevelOrderedQuery(sorted, sort.Parameters[i], fieldExpressions);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static IOrderedQueryable<TEntity> GetFirstOrderLevelQuery<TEntity>(
|
||||
IQueryable<TEntity> query,
|
||||
SortParameter @param,
|
||||
Dictionary<string, Expression<Func<TEntity, object>>> fieldExpressions)
|
||||
{
|
||||
if (!fieldExpressions.TryGetValue(param.ParameterName, out var fieldExpression))
|
||||
{
|
||||
throw new Exception($"{param.ParameterName} is not a sortable field");
|
||||
}
|
||||
|
||||
return param.Order switch
|
||||
{
|
||||
SortOrder.Ascending => query.OrderBy(fieldExpression),
|
||||
SortOrder.Descending => query.OrderByDescending(fieldExpression),
|
||||
_ => throw new InvalidOperationException($"{param.Order} is not a supported value")
|
||||
};
|
||||
}
|
||||
|
||||
private static IOrderedQueryable<TEntity> GetMultiLevelOrderedQuery<TEntity>(
|
||||
IOrderedQueryable<TEntity> query,
|
||||
SortParameter @param,
|
||||
Dictionary<string, Expression<Func<TEntity, object>>> fieldExpressions)
|
||||
{
|
||||
if (!fieldExpressions.TryGetValue(param.ParameterName, out var fieldExpression))
|
||||
{
|
||||
throw new Exception($"{param.ParameterName} is not a sortable field");
|
||||
}
|
||||
|
||||
return param.Order switch
|
||||
{
|
||||
SortOrder.Ascending => query.ThenBy(fieldExpression),
|
||||
SortOrder.Descending => query.ThenByDescending(fieldExpression),
|
||||
_ => throw new InvalidOperationException($"{param.Order} is not a supported value")
|
||||
};
|
||||
}
|
||||
}
|
@ -4,15 +4,18 @@ 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.SortProviders;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CleanArchitecture.Application.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtension
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||
{
|
||||
@ -35,4 +38,12 @@ public static class ServiceCollectionExtension
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddSortProviders(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISortingExpressionProvider<TenantViewModel, Tenant>, TenantViewModelSortProvider>();
|
||||
services.AddScoped<ISortingExpressionProvider<UserViewModel, User>, UserViewModelSortProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.SortProviders;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
|
||||
namespace CleanArchitecture.Application.Interfaces;
|
||||
@ -11,5 +13,10 @@ public interface ITenantService
|
||||
public Task UpdateTenantAsync(UpdateTenantViewModel tenant);
|
||||
public Task DeleteTenantAsync(Guid tenantId);
|
||||
public Task<TenantViewModel?> GetTenantByIdAsync(Guid tenantId);
|
||||
public Task<PagedResult<TenantViewModel>> GetAllTenantsAsync(PageQuery query, string searchTerm = "");
|
||||
|
||||
public Task<PagedResult<TenantViewModel>> GetAllTenantsAsync(
|
||||
PageQuery query,
|
||||
bool includeDeleted,
|
||||
string searchTerm = "",
|
||||
SortQuery? sortQuery = null);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
|
||||
namespace CleanArchitecture.Application.Interfaces;
|
||||
@ -9,7 +10,11 @@ public interface IUserService
|
||||
{
|
||||
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId);
|
||||
public Task<UserViewModel?> GetCurrentUserAsync();
|
||||
public Task<PagedResult<UserViewModel>> GetAllUsersAsync(PageQuery query, string searchTerm = "");
|
||||
public Task<PagedResult<UserViewModel>> GetAllUsersAsync(
|
||||
PageQuery query,
|
||||
bool includeDeleted,
|
||||
string searchTerm = "",
|
||||
SortQuery? sortQuery = null);
|
||||
public Task<Guid> CreateUserAsync(CreateUserViewModel user);
|
||||
public Task UpdateUserAsync(UpdateUserViewModel user);
|
||||
public Task DeleteUserAsync(Guid userId);
|
||||
|
@ -1,8 +1,13 @@
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using MediatR;
|
||||
|
||||
namespace CleanArchitecture.Application.Queries.Tenants.GetAll;
|
||||
|
||||
public sealed record GetAllTenantsQuery(PageQuery Query, string SearchTerm = "") :
|
||||
public sealed record GetAllTenantsQuery(
|
||||
PageQuery Query,
|
||||
bool IncludeDeleted,
|
||||
string SearchTerm = "",
|
||||
SortQuery? SortQuery = null) :
|
||||
IRequest<PagedResult<TenantViewModel>>;
|
@ -1,8 +1,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.Extensions;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -13,10 +16,14 @@ public sealed class GetAllTenantsQueryHandler :
|
||||
IRequestHandler<GetAllTenantsQuery, PagedResult<TenantViewModel>>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository;
|
||||
private readonly ISortingExpressionProvider<TenantViewModel, Tenant> _sortingExpressionProvider;
|
||||
|
||||
public GetAllTenantsQueryHandler(ITenantRepository tenantRepository)
|
||||
public GetAllTenantsQueryHandler(
|
||||
ITenantRepository tenantRepository,
|
||||
ISortingExpressionProvider<TenantViewModel, Tenant> sortingExpressionProvider)
|
||||
{
|
||||
_tenantRepository = tenantRepository;
|
||||
_sortingExpressionProvider = sortingExpressionProvider;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<TenantViewModel>> Handle(
|
||||
@ -26,7 +33,7 @@ public sealed class GetAllTenantsQueryHandler :
|
||||
var tenantsQuery = _tenantRepository
|
||||
.GetAllNoTracking()
|
||||
.Include(x => x.Users)
|
||||
.Where(x => !x.Deleted);
|
||||
.Where(x => request.IncludeDeleted || !x.Deleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
||||
{
|
||||
@ -36,6 +43,8 @@ public sealed class GetAllTenantsQueryHandler :
|
||||
|
||||
var totalCount = await tenantsQuery.CountAsync(cancellationToken);
|
||||
|
||||
tenantsQuery = tenantsQuery.GetOrderedQueryable(request.SortQuery, _sortingExpressionProvider);
|
||||
|
||||
var tenants = await tenantsQuery
|
||||
.Skip((request.Query.Page - 1) * request.Query.PageSize)
|
||||
.Take(request.Query.PageSize)
|
||||
|
@ -1,8 +1,13 @@
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using MediatR;
|
||||
|
||||
namespace CleanArchitecture.Application.Queries.Users.GetAll;
|
||||
|
||||
public sealed record GetAllUsersQuery(PageQuery Query, string SearchTerm = "") :
|
||||
public sealed record GetAllUsersQuery(
|
||||
PageQuery Query,
|
||||
bool IncludeDeleted,
|
||||
string SearchTerm = "",
|
||||
SortQuery? SortQuery = null) :
|
||||
IRequest<PagedResult<UserViewModel>>;
|
@ -1,8 +1,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.Extensions;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -13,10 +16,14 @@ public sealed class GetAllUsersQueryHandler :
|
||||
IRequestHandler<GetAllUsersQuery, PagedResult<UserViewModel>>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ISortingExpressionProvider<UserViewModel, User> _sortingExpressionProvider;
|
||||
|
||||
public GetAllUsersQueryHandler(IUserRepository userRepository)
|
||||
public GetAllUsersQueryHandler(
|
||||
IUserRepository userRepository,
|
||||
ISortingExpressionProvider<UserViewModel, User> sortingExpressionProvider)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_sortingExpressionProvider = sortingExpressionProvider;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<UserViewModel>> Handle(
|
||||
@ -25,7 +32,7 @@ public sealed class GetAllUsersQueryHandler :
|
||||
{
|
||||
var usersQuery = _userRepository
|
||||
.GetAllNoTracking()
|
||||
.Where(x => !x.Deleted);
|
||||
.Where(x => request.IncludeDeleted || !x.Deleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
||||
{
|
||||
@ -37,6 +44,8 @@ public sealed class GetAllUsersQueryHandler :
|
||||
|
||||
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||
|
||||
usersQuery = usersQuery.GetOrderedQueryable(request.SortQuery, _sortingExpressionProvider);
|
||||
|
||||
var users = await usersQuery
|
||||
.Skip((request.Query.Page - 1) * request.Query.PageSize)
|
||||
.Take(request.Query.PageSize)
|
||||
|
@ -3,7 +3,9 @@ using System.Threading.Tasks;
|
||||
using CleanArchitecture.Application.Interfaces;
|
||||
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
||||
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
||||
using CleanArchitecture.Application.SortProviders;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using CleanArchitecture.Domain;
|
||||
using CleanArchitecture.Domain.Commands.Tenants.CreateTenant;
|
||||
@ -64,8 +66,12 @@ public sealed class TenantService : ITenantService
|
||||
return cachedTenant;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<TenantViewModel>> GetAllTenantsAsync(PageQuery query, string searchTerm = "")
|
||||
public async Task<PagedResult<TenantViewModel>> GetAllTenantsAsync(
|
||||
PageQuery query,
|
||||
bool includeDeleted,
|
||||
string searchTerm = "",
|
||||
SortQuery? sortQuery = null)
|
||||
{
|
||||
return await _bus.QueryAsync(new GetAllTenantsQuery(query, searchTerm));
|
||||
return await _bus.QueryAsync(new GetAllTenantsQuery(query, includeDeleted, searchTerm, sortQuery));
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using CleanArchitecture.Application.Interfaces;
|
||||
using CleanArchitecture.Application.Queries.Users.GetAll;
|
||||
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
||||
using CleanArchitecture.Application.ViewModels;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
|
||||
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||
@ -35,9 +36,13 @@ public sealed class UserService : IUserService
|
||||
return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId()));
|
||||
}
|
||||
|
||||
public async Task<PagedResult<UserViewModel>> GetAllUsersAsync(PageQuery query, string searchTerm = "")
|
||||
public async Task<PagedResult<UserViewModel>> GetAllUsersAsync(
|
||||
PageQuery query,
|
||||
bool includeDeleted,
|
||||
string searchTerm = "",
|
||||
SortQuery? sortQuery = null)
|
||||
{
|
||||
return await _bus.QueryAsync(new GetAllUsersQuery(query, searchTerm));
|
||||
return await _bus.QueryAsync(new GetAllUsersQuery(query, includeDeleted, searchTerm, sortQuery));
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateUserAsync(CreateUserViewModel user)
|
||||
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
|
||||
namespace CleanArchitecture.Application.SortProviders;
|
||||
|
||||
public sealed class TenantViewModelSortProvider : ISortingExpressionProvider<TenantViewModel, Tenant>
|
||||
{
|
||||
private static readonly Dictionary<string, Expression<Func<Tenant, object>>> s_expressions = new()
|
||||
{
|
||||
{ "id", tenant => tenant.Id },
|
||||
{ "name", tenant => tenant.Name },
|
||||
};
|
||||
|
||||
public Dictionary<string, Expression<Func<Tenant, object>>> GetSortingExpressions()
|
||||
{
|
||||
return s_expressions;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||
using CleanArchitecture.Application.ViewModels.Users;
|
||||
using CleanArchitecture.Domain.Entities;
|
||||
|
||||
namespace CleanArchitecture.Application.SortProviders;
|
||||
|
||||
public sealed class UserViewModelSortProvider : ISortingExpressionProvider<UserViewModel, User>
|
||||
{
|
||||
private static readonly Dictionary<string, Expression<Func<User, object>>> s_expressions = new()
|
||||
{
|
||||
{ "email", user => user.Email },
|
||||
{ "firstName", user => user.FirstName },
|
||||
{ "lastName", user => user.LastName },
|
||||
{ "tenantId", user => user.TenantId },
|
||||
{ "lastloggedindate", user => user.LastLoggedinDate ?? DateTimeOffset.MinValue },
|
||||
{ "role", user => user.Role },
|
||||
{ "status", user => user.Status }
|
||||
};
|
||||
|
||||
public Dictionary<string, Expression<Func<User, object>>> GetSortingExpressions()
|
||||
{
|
||||
return s_expressions;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace CleanArchitecture.Application.ViewModels.Sorting;
|
||||
|
||||
public interface ISortingExpressionProvider<TViewModel, TEntity>
|
||||
{
|
||||
Dictionary<string, Expression<Func<TEntity, object>>> GetSortingExpressions();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace CleanArchitecture.Application.ViewModels.Sorting;
|
||||
|
||||
public enum SortOrder
|
||||
{
|
||||
Ascending = 0,
|
||||
Descending = 1
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
namespace CleanArchitecture.Application.ViewModels.Sorting;
|
||||
|
||||
public readonly struct SortParameter
|
||||
{
|
||||
public SortOrder Order { get; }
|
||||
public string ParameterName { get; }
|
||||
|
||||
public SortParameter(string parameterName, SortOrder order)
|
||||
{
|
||||
Order = order;
|
||||
ParameterName = parameterName;
|
||||
}
|
||||
}
|
301
CleanArchitecture.Application/ViewModels/Sorting/SortQuery.cs
Normal file
301
CleanArchitecture.Application/ViewModels/Sorting/SortQuery.cs
Normal file
@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CleanArchitecture.Application.ViewModels.Sorting;
|
||||
|
||||
public sealed class SortQuery
|
||||
{
|
||||
private readonly struct QueryInfo
|
||||
{
|
||||
public readonly short PlusSignIndex;
|
||||
public readonly short MinusSignIndex;
|
||||
public readonly short FirstSpaceIndex;
|
||||
public readonly short OpeningBracketIndex;
|
||||
public readonly short ClosingBracketIndex;
|
||||
|
||||
public QueryInfo(
|
||||
short plusSignIndex,
|
||||
short minusSignIndex,
|
||||
short firstSpaceIndex,
|
||||
short openingBracketIndex,
|
||||
short closingBracketIndex)
|
||||
{
|
||||
PlusSignIndex = plusSignIndex;
|
||||
MinusSignIndex = minusSignIndex;
|
||||
FirstSpaceIndex = firstSpaceIndex;
|
||||
OpeningBracketIndex = openingBracketIndex;
|
||||
ClosingBracketIndex = closingBracketIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private string? _query = string.Empty;
|
||||
|
||||
private ReadOnlyCollection<SortParameter> _parameters = new(Array.Empty<SortParameter>());
|
||||
|
||||
public ReadOnlyCollection<SortParameter> Parameters => _parameters;
|
||||
|
||||
[FromQuery(Name = "order_by")]
|
||||
public string? Query
|
||||
{
|
||||
get => _query;
|
||||
set
|
||||
{
|
||||
_query = value;
|
||||
_parameters = ParseQuery(_query);
|
||||
}
|
||||
}
|
||||
|
||||
public static ReadOnlyCollection<SortParameter> ParseQuery(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return new ReadOnlyCollection<SortParameter>(Array.Empty<SortParameter>());
|
||||
}
|
||||
|
||||
if (value.Length > short.MaxValue)
|
||||
{
|
||||
throw new ArgumentException($"sort query can not be longer than {short.MaxValue} characters");
|
||||
}
|
||||
|
||||
value = value.ToLower();
|
||||
|
||||
if (!value.Contains(','))
|
||||
{
|
||||
return new ReadOnlyCollection<SortParameter>(new[] { GetParam(value) });
|
||||
}
|
||||
|
||||
var @params = value.Split(',');
|
||||
var parsedParams = new SortParameter[@params.Length];
|
||||
|
||||
for (int i = 0; i < @params.Length; i++)
|
||||
{
|
||||
parsedParams[i] = GetParam(@params[i]);
|
||||
}
|
||||
|
||||
return new ReadOnlyCollection<SortParameter>(parsedParams);
|
||||
}
|
||||
|
||||
private static SortParameter GetParam(string value)
|
||||
{
|
||||
value = value.Trim();
|
||||
|
||||
var queryInfo = FindTokens(value);
|
||||
|
||||
if (queryInfo.OpeningBracketIndex > 0)
|
||||
{
|
||||
// asc(name), desc(name), ascending(name), descending(name)
|
||||
return GetSortParamFromFunctionalStyle(value, queryInfo);
|
||||
}
|
||||
|
||||
// i.e. "name asc", "name descending" or similar
|
||||
if (queryInfo.FirstSpaceIndex >= 0)
|
||||
{
|
||||
return GetSortParamFromSentence(value, queryInfo);
|
||||
}
|
||||
|
||||
// name, +name, -name, name+, name-
|
||||
return GetSortParamFromSingleWord(value, queryInfo);
|
||||
}
|
||||
|
||||
private static SortParameter GetSortParamFromSentence(string value, QueryInfo info)
|
||||
{
|
||||
var secondWordStartIndex = FindNextNonWhitespaceCharacter(value, info.FirstSpaceIndex + 1);
|
||||
|
||||
if (secondWordStartIndex < 0)
|
||||
{
|
||||
throw new ArgumentException("Expected query string in form of \"{param} asc/desc\"");
|
||||
}
|
||||
|
||||
var paramName = value[..info.FirstSpaceIndex];
|
||||
var orderName = value[secondWordStartIndex..];
|
||||
|
||||
if (orderName == "asc" || orderName == "ascending")
|
||||
{
|
||||
return new SortParameter(paramName, SortOrder.Ascending);
|
||||
}
|
||||
else if (orderName == "desc" || orderName == "descending")
|
||||
{
|
||||
return new SortParameter(paramName, SortOrder.Descending);
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
$"Unsupported sort order {orderName}. Valid are 'asc', 'ascending', 'desc' or 'descending'");
|
||||
}
|
||||
|
||||
private static SortParameter GetSortParamFromSingleWord(string value, QueryInfo info)
|
||||
{
|
||||
if (info.PlusSignIndex < 0 && info.MinusSignIndex < 0)
|
||||
{
|
||||
return new SortParameter(value, SortOrder.Ascending);
|
||||
}
|
||||
|
||||
var order = info.PlusSignIndex >= 0 ? SortOrder.Ascending : SortOrder.Descending;
|
||||
var indicatorIndex = Math.Max(info.MinusSignIndex, info.PlusSignIndex);
|
||||
|
||||
if (indicatorIndex == 0)
|
||||
{
|
||||
return new SortParameter(value[1..], order);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SortParameter(value[..indicatorIndex], order);
|
||||
}
|
||||
}
|
||||
|
||||
private static SortParameter GetSortParamFromFunctionalStyle(string value, QueryInfo info)
|
||||
{
|
||||
var param = value
|
||||
.Substring(info.OpeningBracketIndex + 1, info.ClosingBracketIndex - info.OpeningBracketIndex - 1)
|
||||
.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(param))
|
||||
{
|
||||
throw new FormatException("Parameter name could not be extracted");
|
||||
}
|
||||
|
||||
if (value.StartsWith("asc(") || value.StartsWith("ascending("))
|
||||
{
|
||||
return new SortParameter(param, SortOrder.Ascending);
|
||||
}
|
||||
else if (value.StartsWith("desc(") || value.StartsWith("descending("))
|
||||
{
|
||||
return new SortParameter(param, SortOrder.Descending);
|
||||
}
|
||||
|
||||
throw new FormatException("Unparsable sort query");
|
||||
}
|
||||
|
||||
private static QueryInfo FindTokens(string query)
|
||||
{
|
||||
short plusSignIndex = -1;
|
||||
short minusSignIndex = -1;
|
||||
short firstSpaceIndex = -1;
|
||||
short openingBracketIndex = -1;
|
||||
short closingBracketIndex = -1;
|
||||
|
||||
for (short i = 0; i < query.Length; i++)
|
||||
{
|
||||
switch (query[i])
|
||||
{
|
||||
case '(':
|
||||
if (openingBracketIndex >= 0)
|
||||
{
|
||||
throw new FormatException("Only one bracket is allowed in functional style queries");
|
||||
}
|
||||
|
||||
if (plusSignIndex >= 0 || minusSignIndex >= 0)
|
||||
{
|
||||
throw new FormatException(
|
||||
"Order indicator (\"+\", \"-\") can not be used together with functional style (i.e.\"(name)\")");
|
||||
}
|
||||
|
||||
if (firstSpaceIndex >= 0)
|
||||
{
|
||||
throw new FormatException($"Unexpected whitespace at position {firstSpaceIndex + 1}");
|
||||
}
|
||||
|
||||
openingBracketIndex = i;
|
||||
break;
|
||||
|
||||
case ')':
|
||||
if (closingBracketIndex >= 0)
|
||||
{
|
||||
throw new FormatException("Only one closing bracket is allowed in functional style queries");
|
||||
}
|
||||
|
||||
if (openingBracketIndex < 0)
|
||||
{
|
||||
throw new FormatException("Closing brackets can only be places after opening brackets");
|
||||
}
|
||||
|
||||
closingBracketIndex = i;
|
||||
break;
|
||||
|
||||
case '+':
|
||||
if (plusSignIndex >= 0)
|
||||
{
|
||||
throw new FormatException("Only one positive order indicator \"+\" is allowed per query");
|
||||
}
|
||||
|
||||
if (minusSignIndex >= 0)
|
||||
{
|
||||
throw new FormatException("Only one order indicator (\"+\", \"-\") is allowed per query");
|
||||
}
|
||||
|
||||
if (firstSpaceIndex >= 0)
|
||||
{
|
||||
throw new FormatException($"Unexpected whitespace at position {firstSpaceIndex + 1}");
|
||||
}
|
||||
|
||||
plusSignIndex = i;
|
||||
break;
|
||||
|
||||
case '-':
|
||||
if (minusSignIndex >= 0)
|
||||
{
|
||||
throw new FormatException("Only one negative order indicator \"-\" is allowed per query");
|
||||
}
|
||||
|
||||
if (plusSignIndex >= 0)
|
||||
{
|
||||
throw new FormatException("Only one order indicator (\"+\", \"-\") is allowed per query");
|
||||
}
|
||||
|
||||
if (firstSpaceIndex >= 0)
|
||||
{
|
||||
throw new FormatException($"Unexpected whitespace at position {firstSpaceIndex + 1}");
|
||||
}
|
||||
|
||||
minusSignIndex = i;
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
if (firstSpaceIndex == -1)
|
||||
{
|
||||
firstSpaceIndex = i;
|
||||
}
|
||||
|
||||
if (minusSignIndex >= 0 || plusSignIndex >= 0)
|
||||
{
|
||||
throw new FormatException($"Unexpected whitespace at position {i + 1}");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Check for stuff after query end like "asc(name)blabla
|
||||
// "+" and "-" can be either at the start or at the end, we are only interested
|
||||
// in the case where it's at the end.
|
||||
if (plusSignIndex > 0 || minusSignIndex > 0 || closingBracketIndex >= 0)
|
||||
{
|
||||
var endOfQuery = Math.Max(Math.Max(plusSignIndex, minusSignIndex), closingBracketIndex);
|
||||
|
||||
throw new FormatException($"End of query expected at {endOfQuery}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new QueryInfo(
|
||||
plusSignIndex,
|
||||
minusSignIndex,
|
||||
firstSpaceIndex,
|
||||
openingBracketIndex,
|
||||
closingBracketIndex);
|
||||
}
|
||||
|
||||
private static int FindNextNonWhitespaceCharacter(string value, int startIndex)
|
||||
{
|
||||
for (int i = startIndex; i < value.Length; i++)
|
||||
{
|
||||
if (!char.IsWhiteSpace(value[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user