使用.NET 6開發TodoList應用(14)——實現查詢過濾


系列導航及源代碼

需求

在查詢請求中,還有一類常見的場景是過濾查詢,也就是有限制條件的查詢,落在數據庫層面就是常用的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過濾條件時

  • 請求
    image

  • 響應
    image

請求僅攜帶PriorityLevel過濾條件時

  • 請求
    image

  • 響應
    image

請求攜帶完整的過濾條件時

  • 請求
    image

  • 響應
    image

請求參數不合法時

  • 請求
    image
    我將pageNumber傳成了0。

  • 響應
    image

總結

對於查詢過濾這個需求來說,實現起來還是很簡單的,但是這里其實隱藏了一個很重要的問題:如果查詢的參數太多,比如存在多個Guid類型的字段過濾,URL的總長度是有可能超出瀏覽器或者服務器Host環境限制的,在這種情況下,我們是否還要堅持使用符合理查森成熟度模型Level 2的GET請求呢?

關於這個問題的爭論一直以來就沒有停過,首先說我個人的結論:可以采用POST的方式去變通,沒必要為難自己(雖然這話肯定會引來Restful擁躉的嘲諷)。可是如果對成熟度有硬性的要求,我們如何實現?比較通用的解決方案是將一步GET查詢拆成兩步GET查詢。但是更多的情況,是我認為如果出現了這種情況,就放棄Restful風格吧。

其實GraphQL才是王道。

參考資料

  1. 理查森成熟度模型
  2. GraphQL


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM