diff --git a/src/HelloShop.IdentityService/Controllers/UsersController.cs b/src/HelloShop.IdentityService/Controllers/UsersController.cs index 9860aef..ca8c727 100644 --- a/src/HelloShop.IdentityService/Controllers/UsersController.cs +++ b/src/HelloShop.IdentityService/Controllers/UsersController.cs @@ -8,7 +8,7 @@ using HelloShop.ServiceDefaults.Models.Paging; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System.Collections; +using HelloShop.ServiceDefaults.Extensions; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 @@ -22,13 +22,21 @@ namespace HelloShop.IdentityService.Controllers [Authorize(IdentityPermissions.Users.Default)] public async Task>> GetUsers([FromQuery] UserListRequest model) { - var userList = await dbContext.Set().Skip((model.PageNumber - 1) * model.PageSize).Take(model.PageSize).ToListAsync(); + IQueryable users = dbContext.Set(); - var result = mapper.Map>(userList); + if (model.Keyword is not null) + { + users = users.Where(e => e.UserName != null && e.UserName.Contains(model.Keyword)); + } + + users = users.WhereIf(model.PhoneNumber is not null, e => e.PhoneNumber == model.PhoneNumber); - var responseModel = new PagedResponse(result, result.Count); + IQueryable pagedUsers = users.SortAndPageBy(model); - return Ok(responseModel); + List pagedUserList = await pagedUsers.ToListAsync(); + int totalCount = await users.CountAsync(); + + return new PagedResponse(mapper.Map>(pagedUserList), totalCount); } [HttpGet("{id}")] @@ -120,7 +128,7 @@ namespace HelloShop.IdentityService.Controllers { dbContext.Remove(user); - await dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); return NoContent(); } diff --git a/src/HelloShop.IdentityService/DataSeeding/UserDataSeedingProvider.cs b/src/HelloShop.IdentityService/DataSeeding/UserDataSeedingProvider.cs index 538796e..75f01e7 100644 --- a/src/HelloShop.IdentityService/DataSeeding/UserDataSeedingProvider.cs +++ b/src/HelloShop.IdentityService/DataSeeding/UserDataSeedingProvider.cs @@ -53,6 +53,19 @@ namespace HelloShop.IdentityService.DataSeeding } await userManager.AddToRoleAsync(guestUser, "GuestRole"); + + if (userManager.Users.Count() < 30) + { + for (int i = 0; i < 30; i++) + { + var user = new User + { + UserName = $"user{i}", + Email = $"test{i}@test.com", + }; + await userManager.CreateAsync(user, user.UserName); + } + } } } } diff --git a/src/HelloShop.ServiceDefaults/Extensions/QueryableExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/QueryableExtensions.cs new file mode 100644 index 0000000..e0a6cf8 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Extensions/QueryableExtensions.cs @@ -0,0 +1,78 @@ +using HelloShop.ServiceDefaults.Constants; +using HelloShop.ServiceDefaults.Models.Paging; +using System.Linq.Expressions; +using System.Reflection; + +namespace HelloShop.ServiceDefaults.Extensions; + +public static class QueryableExtensions +{ + public static IQueryable SortBy(this IQueryable query, string? orderBy = null) + { + PropertyInfo[] properties = typeof(TEntity).GetProperties(); + + IOrderedQueryable? orderedQueryable = null; + + if (!string.IsNullOrWhiteSpace(orderBy)) + { + // Convert expressions of the form field1 desc,field2 asc + + string[] orderBySubs = orderBy.Split(','); + + foreach (var orderBySub in orderBySubs) + { + string[] orderByParts = orderBySub.Trim().Split(' '); + + if (orderByParts.Length >= 1) + { + string propertyName = PascalCaseNamingPolicy.PascalCase.ConvertName(orderByParts[0]); + + bool ascending = orderByParts.Length == 1 || (orderByParts.Length == 2 && orderByParts[1].Equals("asc", StringComparison.OrdinalIgnoreCase)); + + if (properties.Any(x => x.Name == propertyName)) + { + if (ascending) + { + orderedQueryable = orderedQueryable is null ? query.OrderBy(propertyName) : orderedQueryable.ThenBy(propertyName); + } + else + { + orderedQueryable = orderedQueryable is null ? query.OrderByDescending(propertyName) : orderedQueryable.ThenByDescending(propertyName); + } + } + } + } + } + + if (orderedQueryable is null) + { + string defaultPropertyName = properties.Any(x => x.Name == EntityConnstants.DefaultKey) ? EntityConnstants.DefaultKey : properties.First().Name; + + orderedQueryable = query.OrderByDescending(defaultPropertyName); + } + + return orderedQueryable; + } + + public static IQueryable PageBy(this IQueryable query, PagedRequest pagedRequest) + { + return query.Skip((pagedRequest.PageNumber - 1) * pagedRequest.PageSize).Take(pagedRequest.PageSize); + } + + public static IQueryable SortAndPageBy(this IQueryable query, PagedAndSortedRequest? pagedAndSortedRequest = null) + { + pagedAndSortedRequest ??= new PagedAndSortedRequest { PageNumber = 1, PageSize = PagingConstants.DefaultPageSize }; + + return query.SortBy(pagedAndSortedRequest.OrderBy).PageBy(pagedAndSortedRequest); + } + + public static IQueryable WhereIf(this IQueryable source, bool condition, Expression> predicate) + { + return condition ? source.Where(predicate) : source; + } + + public static IQueryable WhereIf(this IQueryable source, bool condition, Expression> predicate) + { + return condition ? source.Where(predicate) : source; + } +} diff --git a/src/HelloShop.ServiceDefaults/Extensions/QueryableOrderByExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/QueryableOrderByExtensions.cs new file mode 100644 index 0000000..713e3d0 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Extensions/QueryableOrderByExtensions.cs @@ -0,0 +1,44 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; + +namespace HelloShop.ServiceDefaults.Extensions; + +public static class QueryableOrderByExtensions +{ + public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) => OrderingHelper.OrderBy(source, propertyName); + + public static IOrderedQueryable OrderByDescending(this IQueryable source, string propertyName) => OrderingHelper.OrderByDescending(source, propertyName); + + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string propertyName) => OrderingHelper.ThenBy(source, propertyName); + + public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, string propertyName) => OrderingHelper.ThenByDescending(source, propertyName); + + private static class OrderingHelper + { + private static readonly ConcurrentDictionary cached = new(); + + public static IOrderedQueryable OrderBy(IQueryable source, string propertyName) => Queryable.OrderBy(source, (dynamic)CreateLambdaExpression(propertyName)); + + public static IOrderedQueryable OrderByDescending(IQueryable source, string propertyName) => Queryable.OrderByDescending(source, (dynamic)CreateLambdaExpression(propertyName)); + + public static IOrderedQueryable ThenBy(IOrderedQueryable source, string propertyName) => Queryable.ThenBy(source, (dynamic)CreateLambdaExpression(propertyName)); + + public static IOrderedQueryable ThenByDescending(IOrderedQueryable source, string propertyName) => Queryable.ThenByDescending(source, (dynamic)CreateLambdaExpression(propertyName)); + + private static LambdaExpression CreateLambdaExpression(string propertyName) + { + if (cached.TryGetValue(propertyName, out LambdaExpression? value)) + { + return value; + } + + var parameter = Expression.Parameter(typeof(TSource)); + var body = Expression.Property(parameter, propertyName); + var keySelector = Expression.Lambda(body, parameter); + + cached[propertyName] = keySelector; + + return keySelector; + } + } +} diff --git a/src/HelloShop.ServiceDefaults/Infrastructure/PascalCaseNamingPolicy.cs b/src/HelloShop.ServiceDefaults/Infrastructure/PascalCaseNamingPolicy.cs new file mode 100644 index 0000000..6f0c121 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Infrastructure/PascalCaseNamingPolicy.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace HelloShop.ServiceDefaults; + +public class PascalCaseNamingPolicy : JsonNamingPolicy +{ + public static PascalCaseNamingPolicy PascalCase { get; } = new PascalCaseNamingPolicy(); + + public override string ConvertName(string name) => string.Concat(char.ToUpper(name[0]), name[1..]); +} diff --git a/src/HelloShop.ServiceDefaults/Models/Paging/PagedAndSortedRequest.cs b/src/HelloShop.ServiceDefaults/Models/Paging/PagedAndSortedRequest.cs index 6ec15b8..6afcb2e 100644 --- a/src/HelloShop.ServiceDefaults/Models/Paging/PagedAndSortedRequest.cs +++ b/src/HelloShop.ServiceDefaults/Models/Paging/PagedAndSortedRequest.cs @@ -2,5 +2,5 @@ public class PagedAndSortedRequest : PagedRequest { - public IEnumerable? Sorts { get; init; } -} + public string? OrderBy { get; init; } +} \ No newline at end of file diff --git a/src/HelloShop.ServiceDefaults/Models/Paging/SortingOrder.cs b/src/HelloShop.ServiceDefaults/Models/Paging/SortingOrder.cs deleted file mode 100644 index a618efb..0000000 --- a/src/HelloShop.ServiceDefaults/Models/Paging/SortingOrder.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace HelloShop.ServiceDefaults.Models.Paging; - -public class SortingOrder -{ - public required string PropertyName { get; init; } - - public bool Ascending { get; init; } -}