返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
StarBlog博客开发笔记

StarBlog Development Notes (23): Pagination, Filtering, Search, and Sorting for the Article List API

Preface Quickly filling in the gap I left in the previous post. In part 6, I already gave a preliminary introduction. This article adds some extra details. If you've already got this part working, you can skip ahead: Developing a Blog Project with .NetCore — StarBlog (6): Blog Post List Page Development For a standard Web API, pagination, filtering, search, and sorting are very common features. They make it easier for users to view data and can also improve application performance. Common Request Parameters Define a class to serve as the common request parameters The common parameters for list APIs are these: PageSi

曦远 (DealiAxy)约 29 分钟
目录8 个章节

Preface

Quickly filling in the gap I left in the previous post.

In part 6, I already gave a preliminary introduction. This article adds some extra details. If you've already got this part working, you can skip ahead: Developing a Blog Project with .NetCore — StarBlog (6): Blog Post List Page Development

For a standard Web API, pagination, filtering, search, and sorting are very common features. They make it easier for users to view data and can also improve application performance.

Common Request Parameters

Define a class to serve as the common request parameters.

The common parameters for list APIs are these: PageSize, Page, Search, SortBy

Mapped onto the URL, they look like Blog/?pageSize=10&page=1&search=关键词

public class QueryParameters {
    /// <summary>
    /// 最大页面条目
    /// </summary>
    public const int MaxPageSize = 50;

    private int _pageSize = 10;

    /// <summary>
    /// 页面大小
    /// </summary>
    public int PageSize {
        get => _pageSize;
        set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
    }

    /// <summary>
    /// 当前页码
    /// </summary>
    public int Page { get; set; } = 1;

    /// <summary>
    /// 搜索关键词
    /// </summary>
    public string? Search { get; set; }

    /// <summary>
    /// 排序字段
    /// </summary>
    public string? SortBy { get; set; }
}

Post List Request Parameters

Based on the common request parameters QueryParameters, derive the post list request parameter class PostQueryParameters.

public class PostQueryParameters : QueryParameters {
    /// <summary>
    /// 仅请求已发布文章
    /// </summary>
    public bool OnlyPublished { get; set; } = false;

    /// <summary>
    /// 文章状态
    /// </summary>
    public string? Status { get; set; }
    
    /// <summary>
    /// 分类ID
    /// </summary>
    public int CategoryId { get; set; } = 0;

    /// <summary>
    /// 排序字段
    /// </summary>
    public new string? SortBy { get; set; } = "-LastUpdateTime";
}

On top of the common request parameters, add post-related filter fields.

The SortBy field uses the new keyword to hide the base class property, with the default sort set to last update time. A leading minus sign indicates descending order.

Service

Wrap the method for getting a paged list in StarBlog.Web/Services/PostService.cs.

The code has comments and is fairly easy to follow: it filters according to the various parameters in PostQueryParameters.

public IPagedList<Post> GetPagedList(PostQueryParameters param) {
    var querySet = _postRepo.Select;

    // 是否发布
    if (param.OnlyPublished) {
        querySet = _postRepo.Select.Where(a => a.IsPublish);
    }

    // 状态过滤
    if (!string.IsNullOrEmpty(param.Status)) {
        querySet = querySet.Where(a => a.Status == param.Status);
    }

    // 分类过滤
    if (param.CategoryId != 0) {
        querySet = querySet.Where(a => a.CategoryId == param.CategoryId);
    }

    // 关键词过滤
    if (!string.IsNullOrEmpty(param.Search)) {
        querySet = querySet.Where(a => a.Title.Contains(param.Search));
    }

    // 排序
    if (!string.IsNullOrEmpty(param.SortBy)) {
        // 是否升序
        var isAscending = !param.SortBy.StartsWith("-");
        var orderByProperty = param.SortBy.Trim('-');

        querySet = querySet.OrderByPropertyName(orderByProperty, isAscending);
    }

    return querySet.Include(a => a.Category).ToList()
        .ToPagedList(param.Page, param.PageSize);
}

Search Implementation

In the service code above

you can see that search is just simple “keyword filtering”.

It uses Title.Contains(param.Search), which converts to SQL as

select * from post where title like '%关键词%'

It simply checks whether the title string contains the keyword as a substring.

This is enough for a simple article search. If you want to search article content the way search engines like Google or Baidu do, you need full-text search.

The mainstream options today are ElasticSearch and Solr. We can consider adding this feature to the project later~

P.S. On full-text search, I previously wrote an article: Principles of Full-Text Search Engines and a Brief Introduction to Lucene

I also open-sourced a toy-level full-text search engine: https://github.com/Deali-Axy/CloverSearch

Pagination Implementation

This project uses X.PagedList to implement pagination.

This component is convenient when used with MVC. For a pure Web API, using the database’s built-in pagination is a better choice and performs better.

This pagination component adds an extension method ToPagedList on IEnumerable<T>, so when using it you have to read all the data first and then paginate. Performance is not as good as doing pagination in the database and then reading the results. Many ORMs support this, and FreeSQL is no exception.

Usage example:

var list = fsql.Select<Topic>()
    .Where(a => a.Id > 10)
    .Count(out var total) //总记录数量
    .Page(1, 20)
    .Tolist();

For details, see the FreeSQL official documentation: https://freesql.net/guide/paging.html

Once you use the X.PagedList component, calling the ToPagedList method on any IEnumerable<T> object yields an IPagedList<T> object.

This object holds the list data for the current page, as well as pagination information.

To make this information convenient for the frontend to use, I also wrote an extension method.

StarBlog.Web/Extensions/PagedListExt.cs

public static class PagedListExt {
    public static PaginationMetadata ToPaginationMetadata(this IPagedList page) {
        return new PaginationMetadata {
            PageCount = page.PageCount,
            TotalItemCount = page.TotalItemCount,
            PageNumber = page.PageNumber,
            PageSize = page.PageSize,
            HasNextPage = page.HasNextPage,
            HasPreviousPage = page.HasPreviousPage,
            IsFirstPage = page.IsFirstPage,
            IsLastPage = page.IsLastPage,
            FirstItemOnPage = page.FirstItemOnPage,
            LastItemOnPage = page.LastItemOnPage
        };
    }

    public static string ToPaginationMetadataJson(this IPagedList page) {
        return JsonSerializer.Serialize(ToPaginationMetadata(page));
    }
}

That way you can call ToPaginationMetadata on the IPagedList object you get after pagination to obtain the pagination metadata.

This PaginationMetadata is also a ViewModel defined in this project, StarBlog.Web/ViewModels/PaginationMetadata.cs

The code is as follows

public class PaginationMetadata {
    public int PageCount { get; set; }
    public int TotalItemCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public bool HasPreviousPage { get; set; }
    public bool HasNextPage { get; set; }
    public bool IsFirstPage { get; set; }
    public bool IsLastPage { get; set; }
    public int FirstItemOnPage { get; set; }
    public int LastItemOnPage { get; set; }
}

Done.

Controller and Final Result

The code is as follows

[AllowAnonymous]
[HttpGet]
public ApiResponsePaged<Post> GetList([FromQuery] PostQueryParameters param) {
    var pagedList = _postService.GetPagedList(param);
    return new ApiResponsePaged<Post> {
        Message = "Get posts list",
        Data = pagedList.ToList(),
        Pagination = pagedList.ToPaginationMetadata()
    };
}

After obtaining the paged data, return a value of type ApiResponsePaged<T>.

This is also an API response type I encapsulated; the next article will cover it in detail.

The Data property is the list data, and the Pagination property is the pagination information.

A request to this API returns something like the following

{
  "pagination": {
    "pageCount": 40,
    "totalItemCount": 394,
    "pageNumber": 1,
    "pageSize": 10,
    "hasPreviousPage": false,
    "hasNextPage": true,
    "isFirstPage": true,
    "isLastPage": false,
    "firstItemOnPage": 1,
    "lastItemOnPage": 10
  },
  "statusCode": 200,
  "successful": true,
  "message": "Get posts list",
  "data": [{...},{...},{...},{...},{...}]
}

Series Articles

_EOF_

StarBlog Development Notes (23): Pagination, Filtering, Search, and Sorting for the Article List API

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

StarBlog博客开发笔记(21):开始开发RESTFul接口

前言 最近电脑坏了,开源项目的进度也受到一些影响 这篇酝酿很久了,作为本系列第二部分(API接口开发)的第一篇,得想一个好的开头,想着想着就鸽了好久,索性不扯那么多了,直接开写吧~ 关于RESTFul 网上很多相关的文章都要把RESTFul历史来龙去脉给复制一遍,所以我这就不重复了,现在主要的HTTP接口风格就俩:RPC和RESTFul。 举个例子就可以看出这俩的区别 RPC风格 分别是增删改查的

2023-01-02 · StarBlog博客开发笔记 · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…