mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-07-04 21:14:00 +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.Api.Models;
|
||||||
using CleanArchitecture.Application.Interfaces;
|
using CleanArchitecture.Application.Interfaces;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
using CleanArchitecture.Domain.Notifications;
|
using CleanArchitecture.Domain.Notifications;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@ -31,11 +32,15 @@ public sealed class TenantController : ApiController
|
|||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<TenantViewModel>>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<TenantViewModel>>))]
|
||||||
public async Task<IActionResult> GetAllTenantsAsync(
|
public async Task<IActionResult> GetAllTenantsAsync(
|
||||||
[FromQuery] PageQuery query,
|
[FromQuery] PageQuery query,
|
||||||
[FromQuery] string searchTerm = "")
|
[FromQuery] string searchTerm = "",
|
||||||
|
[FromQuery] bool includeDeleted = false,
|
||||||
|
[FromQuery] SortQuery? sortQuery = null)
|
||||||
{
|
{
|
||||||
var tenants = await _tenantService.GetAllTenantsAsync(
|
var tenants = await _tenantService.GetAllTenantsAsync(
|
||||||
query,
|
query,
|
||||||
searchTerm);
|
includeDeleted,
|
||||||
|
searchTerm,
|
||||||
|
sortQuery);
|
||||||
return Response(tenants);
|
return Response(tenants);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using CleanArchitecture.Api.Models;
|
using CleanArchitecture.Api.Models;
|
||||||
using CleanArchitecture.Application.Interfaces;
|
using CleanArchitecture.Application.Interfaces;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
using CleanArchitecture.Domain.Notifications;
|
using CleanArchitecture.Domain.Notifications;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@ -31,11 +32,15 @@ public sealed class UserController : ApiController
|
|||||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<UserViewModel>>))]
|
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<PagedResult<UserViewModel>>))]
|
||||||
public async Task<IActionResult> GetAllUsersAsync(
|
public async Task<IActionResult> GetAllUsersAsync(
|
||||||
[FromQuery] PageQuery query,
|
[FromQuery] PageQuery query,
|
||||||
[FromQuery] string searchTerm = "")
|
[FromQuery] string searchTerm = "",
|
||||||
|
[FromQuery] bool includeDeleted = false,
|
||||||
|
[FromQuery] SortQuery? sortQuery = null)
|
||||||
{
|
{
|
||||||
var users = await _userService.GetAllUsersAsync(
|
var users = await _userService.GetAllUsersAsync(
|
||||||
query,
|
query,
|
||||||
searchTerm);
|
includeDeleted,
|
||||||
|
searchTerm,
|
||||||
|
sortQuery);
|
||||||
return Response(users);
|
return Response(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ builder.Services.AddAuth(builder.Configuration);
|
|||||||
builder.Services.AddInfrastructure(builder.Configuration, "CleanArchitecture.Infrastructure");
|
builder.Services.AddInfrastructure(builder.Configuration, "CleanArchitecture.Infrastructure");
|
||||||
builder.Services.AddQueryHandlers();
|
builder.Services.AddQueryHandlers();
|
||||||
builder.Services.AddServices();
|
builder.Services.AddServices();
|
||||||
|
builder.Services.AddSortProviders();
|
||||||
builder.Services.AddCommandHandlers();
|
builder.Services.AddCommandHandlers();
|
||||||
builder.Services.AddNotificationHandlers();
|
builder.Services.AddNotificationHandlers();
|
||||||
builder.Services.AddApiUser();
|
builder.Services.AddApiUser();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
||||||
|
using CleanArchitecture.Application.SortProviders;
|
||||||
using CleanArchitecture.Domain.Entities;
|
using CleanArchitecture.Domain.Entities;
|
||||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
using MockQueryable.NSubstitute;
|
using MockQueryable.NSubstitute;
|
||||||
@ -16,8 +17,9 @@ public sealed class GetAllTenantsTestFixture : QueryHandlerBaseFixture
|
|||||||
public GetAllTenantsTestFixture()
|
public GetAllTenantsTestFixture()
|
||||||
{
|
{
|
||||||
TenantRepository = Substitute.For<ITenantRepository>();
|
TenantRepository = Substitute.For<ITenantRepository>();
|
||||||
|
var sortingProvider = new TenantViewModelSortProvider();
|
||||||
|
|
||||||
QueryHandler = new GetAllTenantsQueryHandler(TenantRepository);
|
QueryHandler = new GetAllTenantsQueryHandler(TenantRepository, sortingProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tenant SetupTenant(bool deleted = false)
|
public Tenant SetupTenant(bool deleted = false)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using CleanArchitecture.Application.Queries.Users.GetAll;
|
using CleanArchitecture.Application.Queries.Users.GetAll;
|
||||||
|
using CleanArchitecture.Application.SortProviders;
|
||||||
using CleanArchitecture.Domain.Entities;
|
using CleanArchitecture.Domain.Entities;
|
||||||
using CleanArchitecture.Domain.Enums;
|
using CleanArchitecture.Domain.Enums;
|
||||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
@ -17,8 +18,9 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
|
|||||||
public GetAllUsersTestFixture()
|
public GetAllUsersTestFixture()
|
||||||
{
|
{
|
||||||
UserRepository = Substitute.For<IUserRepository>();
|
UserRepository = Substitute.For<IUserRepository>();
|
||||||
|
var sortingProvider = new UserViewModelSortProvider();
|
||||||
|
|
||||||
Handler = new GetAllUsersQueryHandler(UserRepository);
|
Handler = new GetAllUsersQueryHandler(UserRepository, sortingProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User SetupUserAsync()
|
public User SetupUserAsync()
|
||||||
|
@ -24,7 +24,7 @@ public sealed class GetAllTenantsQueryHandlerTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
var result = await _fixture.QueryHandler.Handle(
|
var result = await _fixture.QueryHandler.Handle(
|
||||||
new GetAllTenantsQuery(query),
|
new GetAllTenantsQuery(query, false),
|
||||||
default);
|
default);
|
||||||
|
|
||||||
_fixture.VerifyNoDomainNotification();
|
_fixture.VerifyNoDomainNotification();
|
||||||
@ -48,7 +48,7 @@ public sealed class GetAllTenantsQueryHandlerTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
var result = await _fixture.QueryHandler.Handle(
|
var result = await _fixture.QueryHandler.Handle(
|
||||||
new GetAllTenantsQuery(query),
|
new GetAllTenantsQuery(query, false),
|
||||||
default);
|
default);
|
||||||
|
|
||||||
result.PageSize.Should().Be(query.PageSize);
|
result.PageSize.Should().Be(query.PageSize);
|
||||||
|
@ -24,7 +24,7 @@ public sealed class GetAllUsersQueryHandlerTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
var result = await _fixture.Handler.Handle(
|
var result = await _fixture.Handler.Handle(
|
||||||
new GetAllUsersQuery(query, user.Email),
|
new GetAllUsersQuery(query, false, user.Email),
|
||||||
default);
|
default);
|
||||||
|
|
||||||
_fixture.VerifyNoDomainNotification();
|
_fixture.VerifyNoDomainNotification();
|
||||||
@ -51,7 +51,7 @@ public sealed class GetAllUsersQueryHandlerTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
var result = await _fixture.Handler.Handle(
|
var result = await _fixture.Handler.Handle(
|
||||||
new GetAllUsersQuery(query),
|
new GetAllUsersQuery(query, false),
|
||||||
default);
|
default);
|
||||||
|
|
||||||
_fixture.VerifyNoDomainNotification();
|
_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.GetAll;
|
||||||
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
||||||
using CleanArchitecture.Application.Services;
|
using CleanArchitecture.Application.Services;
|
||||||
|
using CleanArchitecture.Application.SortProviders;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Extensions;
|
namespace CleanArchitecture.Application.Extensions;
|
||||||
|
|
||||||
public static class ServiceCollectionExtension
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddServices(this IServiceCollection services)
|
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
@ -35,4 +38,12 @@ public static class ServiceCollectionExtension
|
|||||||
|
|
||||||
return services;
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Application.SortProviders;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Interfaces;
|
namespace CleanArchitecture.Application.Interfaces;
|
||||||
@ -11,5 +13,10 @@ public interface ITenantService
|
|||||||
public Task UpdateTenantAsync(UpdateTenantViewModel tenant);
|
public Task UpdateTenantAsync(UpdateTenantViewModel tenant);
|
||||||
public Task DeleteTenantAsync(Guid tenantId);
|
public Task DeleteTenantAsync(Guid tenantId);
|
||||||
public Task<TenantViewModel?> GetTenantByIdAsync(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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Interfaces;
|
namespace CleanArchitecture.Application.Interfaces;
|
||||||
@ -9,7 +10,11 @@ public interface IUserService
|
|||||||
{
|
{
|
||||||
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId);
|
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId);
|
||||||
public Task<UserViewModel?> GetCurrentUserAsync();
|
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<Guid> CreateUserAsync(CreateUserViewModel user);
|
||||||
public Task UpdateUserAsync(UpdateUserViewModel user);
|
public Task UpdateUserAsync(UpdateUserViewModel user);
|
||||||
public Task DeleteUserAsync(Guid userId);
|
public Task DeleteUserAsync(Guid userId);
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Queries.Tenants.GetAll;
|
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>>;
|
IRequest<PagedResult<TenantViewModel>>;
|
@ -1,8 +1,11 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Application.Extensions;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -13,10 +16,14 @@ public sealed class GetAllTenantsQueryHandler :
|
|||||||
IRequestHandler<GetAllTenantsQuery, PagedResult<TenantViewModel>>
|
IRequestHandler<GetAllTenantsQuery, PagedResult<TenantViewModel>>
|
||||||
{
|
{
|
||||||
private readonly ITenantRepository _tenantRepository;
|
private readonly ITenantRepository _tenantRepository;
|
||||||
|
private readonly ISortingExpressionProvider<TenantViewModel, Tenant> _sortingExpressionProvider;
|
||||||
|
|
||||||
public GetAllTenantsQueryHandler(ITenantRepository tenantRepository)
|
public GetAllTenantsQueryHandler(
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ISortingExpressionProvider<TenantViewModel, Tenant> sortingExpressionProvider)
|
||||||
{
|
{
|
||||||
_tenantRepository = tenantRepository;
|
_tenantRepository = tenantRepository;
|
||||||
|
_sortingExpressionProvider = sortingExpressionProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedResult<TenantViewModel>> Handle(
|
public async Task<PagedResult<TenantViewModel>> Handle(
|
||||||
@ -26,7 +33,7 @@ public sealed class GetAllTenantsQueryHandler :
|
|||||||
var tenantsQuery = _tenantRepository
|
var tenantsQuery = _tenantRepository
|
||||||
.GetAllNoTracking()
|
.GetAllNoTracking()
|
||||||
.Include(x => x.Users)
|
.Include(x => x.Users)
|
||||||
.Where(x => !x.Deleted);
|
.Where(x => request.IncludeDeleted || !x.Deleted);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
||||||
{
|
{
|
||||||
@ -36,6 +43,8 @@ public sealed class GetAllTenantsQueryHandler :
|
|||||||
|
|
||||||
var totalCount = await tenantsQuery.CountAsync(cancellationToken);
|
var totalCount = await tenantsQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
tenantsQuery = tenantsQuery.GetOrderedQueryable(request.SortQuery, _sortingExpressionProvider);
|
||||||
|
|
||||||
var tenants = await tenantsQuery
|
var tenants = await tenantsQuery
|
||||||
.Skip((request.Query.Page - 1) * request.Query.PageSize)
|
.Skip((request.Query.Page - 1) * request.Query.PageSize)
|
||||||
.Take(request.Query.PageSize)
|
.Take(request.Query.PageSize)
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace CleanArchitecture.Application.Queries.Users.GetAll;
|
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>>;
|
IRequest<PagedResult<UserViewModel>>;
|
@ -1,8 +1,11 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Application.Extensions;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
using CleanArchitecture.Domain.Interfaces.Repositories;
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -13,10 +16,14 @@ public sealed class GetAllUsersQueryHandler :
|
|||||||
IRequestHandler<GetAllUsersQuery, PagedResult<UserViewModel>>
|
IRequestHandler<GetAllUsersQuery, PagedResult<UserViewModel>>
|
||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ISortingExpressionProvider<UserViewModel, User> _sortingExpressionProvider;
|
||||||
|
|
||||||
public GetAllUsersQueryHandler(IUserRepository userRepository)
|
public GetAllUsersQueryHandler(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
ISortingExpressionProvider<UserViewModel, User> sortingExpressionProvider)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_sortingExpressionProvider = sortingExpressionProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PagedResult<UserViewModel>> Handle(
|
public async Task<PagedResult<UserViewModel>> Handle(
|
||||||
@ -25,7 +32,7 @@ public sealed class GetAllUsersQueryHandler :
|
|||||||
{
|
{
|
||||||
var usersQuery = _userRepository
|
var usersQuery = _userRepository
|
||||||
.GetAllNoTracking()
|
.GetAllNoTracking()
|
||||||
.Where(x => !x.Deleted);
|
.Where(x => request.IncludeDeleted || !x.Deleted);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
|
||||||
{
|
{
|
||||||
@ -37,6 +44,8 @@ public sealed class GetAllUsersQueryHandler :
|
|||||||
|
|
||||||
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
usersQuery = usersQuery.GetOrderedQueryable(request.SortQuery, _sortingExpressionProvider);
|
||||||
|
|
||||||
var users = await usersQuery
|
var users = await usersQuery
|
||||||
.Skip((request.Query.Page - 1) * request.Query.PageSize)
|
.Skip((request.Query.Page - 1) * request.Query.PageSize)
|
||||||
.Take(request.Query.PageSize)
|
.Take(request.Query.PageSize)
|
||||||
|
@ -3,7 +3,9 @@ using System.Threading.Tasks;
|
|||||||
using CleanArchitecture.Application.Interfaces;
|
using CleanArchitecture.Application.Interfaces;
|
||||||
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
using CleanArchitecture.Application.Queries.Tenants.GetAll;
|
||||||
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
|
||||||
|
using CleanArchitecture.Application.SortProviders;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Tenants;
|
using CleanArchitecture.Application.ViewModels.Tenants;
|
||||||
using CleanArchitecture.Domain;
|
using CleanArchitecture.Domain;
|
||||||
using CleanArchitecture.Domain.Commands.Tenants.CreateTenant;
|
using CleanArchitecture.Domain.Commands.Tenants.CreateTenant;
|
||||||
@ -64,8 +66,12 @@ public sealed class TenantService : ITenantService
|
|||||||
return cachedTenant;
|
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.GetAll;
|
||||||
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
using CleanArchitecture.Application.Queries.Users.GetUserById;
|
||||||
using CleanArchitecture.Application.ViewModels;
|
using CleanArchitecture.Application.ViewModels;
|
||||||
|
using CleanArchitecture.Application.ViewModels.Sorting;
|
||||||
using CleanArchitecture.Application.ViewModels.Users;
|
using CleanArchitecture.Application.ViewModels.Users;
|
||||||
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
|
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
|
||||||
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
using CleanArchitecture.Domain.Commands.Users.CreateUser;
|
||||||
@ -35,9 +36,13 @@ public sealed class UserService : IUserService
|
|||||||
return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId()));
|
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)
|
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