系列導航及源代碼
需求
在查詢請求中,還有一類常見的場景是過濾查詢,也就是有限制條件的查詢,落在數據庫層面就是常用的Where
查詢子句。實現起來也很簡單。
目標
實現查詢過濾的功能
原理與思路
查詢過濾的請求有兩種方式,一種是采用POST
方法,將查詢條件放在請求體中,但是這種方式實際上和Restful的動詞語義產生了矛盾,從Restful API成熟度模型的角度來說,還停留在Level 1階段(僅知道地址,不符合動詞語義),詳情參考理查森成熟度模型;還有一種方法是采用GET
方法,將查詢條件放在查詢字符串里,我們將采用第二種方式來實現。
實現
我們還是通過查詢TodoItem
列表來演示查詢過濾,和前面的文章一樣,我們先來實現一個查詢的Query
請求對象:
GetTodoItemsWithConditionQuery.cs
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
using TodoList.Application.Common.Interfaces;
using TodoList.Application.Common.Mappings;
using TodoList.Application.Common.Models;
using TodoList.Domain.Entities;
using TodoList.Domain.Enums;
namespace TodoList.Application.TodoItems.Queries.GetTodoItems;
public class GetTodoItemsWithConditionQuery : IRequest<PaginatedList<TodoItemDto>>
{
public Guid ListId { get; set; }
public bool? Done { get; set; }
public PriorityLevel? PriorityLevel { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
}
public class GetTodoItemsWithConditionQueryHandler : IRequestHandler<GetTodoItemsWithConditionQuery, PaginatedList<TodoItemDto>>
{
private readonly IRepository<TodoItem> _repository;
private readonly IMapper _mapper;
public GetTodoItemsWithConditionQueryHandler(IRepository<TodoItem> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<PaginatedList<TodoItemDto>> Handle(GetTodoItemsWithConditionQuery request, CancellationToken cancellationToken)
{
return await _repository
.GetAsQueryable(x => x.ListId == request.ListId
&& (!request.Done.HasValue || x.Done == request.Done)
&& (!request.PriorityLevel.HasValue || x.Priority == request.PriorityLevel))
.OrderBy(x => x.Title)
.ProjectTo<TodoItemDto>(_mapper.ConfigurationProvider)
.PaginatedListAsync(request.PageNumber, request.PageSize);
}
}
甚至為了結合之前講的請求校驗,我們可以在這里增加一個校驗規則:
GetTodoItemValidator.cs
using FluentValidation;
namespace TodoList.Application.TodoItems.Queries.GetTodoItems;
public class GetTodoItemValidator : AbstractValidator<GetTodoItemsWithConditionQuery>
{
public GetTodoItemValidator()
{
RuleFor(x => x.ListId).NotEmpty().WithMessage("ListId is required.");
RuleFor(x => x.PageNumber).GreaterThanOrEqualTo(1).WithMessage("PageNumber at least greater than or equal to 1.");
RuleFor(x => x.PageSize).GreaterThanOrEqualTo(1).WithMessage("PageSize at least greater than or equal to 1.");
}
}
接下來在TodoItemController
中實現請求處理,我們直接修改上一篇講分頁的那個請求成如下(如果在上一篇的基礎上新增Action的話,會導致路由的歧義):
TodoItemController.cs
// 省略其他...
[HttpGet]
public async Task<ApiResponse<PaginatedList<TodoItemDto>>> GetTodoItemsWithCondition([FromQuery] GetTodoItemsWithConditionQuery query)
{
return ApiResponse<PaginatedList<TodoItemDto>>.Success(await _mediator.Send(query));
}
驗證
啟動Api
項目,執行創建TodoList
的請求:
請求僅攜帶Done
過濾條件時
-
請求
-
響應
請求僅攜帶PriorityLevel
過濾條件時
-
請求
-
響應
請求攜帶完整的過濾條件時
-
請求
-
響應
請求參數不合法時
-
請求
我將pageNumber傳成了0。 -
響應
總結
對於查詢過濾這個需求來說,實現起來還是很簡單的,但是這里其實隱藏了一個很重要的問題:如果查詢的參數太多,比如存在多個Guid
類型的字段過濾,URL的總長度是有可能超出瀏覽器或者服務器Host環境限制的,在這種情況下,我們是否還要堅持使用符合理查森成熟度模型Level 2的GET
請求呢?
關於這個問題的爭論一直以來就沒有停過,首先說我個人的結論:可以采用POST
的方式去變通,沒必要為難自己(雖然這話肯定會引來Restful擁躉的嘲諷)。可是如果對成熟度有硬性的要求,我們如何實現?比較通用的解決方案是將一步GET
查詢拆成兩步GET
查詢。但是更多的情況,是我認為如果出現了這種情況,就放棄Restful風格吧。
其實GraphQL才是王道。