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

Add sorting

This commit is contained in:
alex289 2023-09-09 12:54:33 +02:00
parent 09c21f23a3
commit b54d4f4de5
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
23 changed files with 554 additions and 23 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace CleanArchitecture.Application.ViewModels.Sorting;
public enum SortOrder
{
Ascending = 0,
Descending = 1
}

View File

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

View 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;
}
}