ASP.NET Core 2.2 WebAPI で Pagination対応 する

1. はじめに

ASP.NET Core WebAPIにおいて、pagination(ページング)でJSONデータを返す実装のメモです。
(ググれば既出だけど、意外に情報少なめだったので、自分メモの意味も込めて)

開発環境

※ VS2017でもCore2.xでも同じだと思う。

2. こんな実装をする

pagination対応するときは、主に以下の2つがあります。

  • body要素のjsonにページ番号や全体件数を含ませる
  • body要素には本来のデータのみを含ませ、HTTP Response Headerにページ情報や全体件数を含ませる

ここでは後者の実装を行います。

社員情報をpaginationで5件ずつ取得することができるAPIを想定します。

GET /api/employee?page=10

↑↑↑とすると
↓↓↓が帰るみたいな

[
  {
    "id": 45,
    "firstName": "しゃいん",
    "lastName": "45号"
  },
  {
    "id": 46,
    "firstName": "しゃいん",
    "lastName": "46号"
  },
  {
    "id": 47,
    "firstName": "しゃいん",
    "lastName": "47号"
  },
  {
    "id": 48,
    "firstName": "しゃいん",
    "lastName": "48号"
  },
  {
    "id": 49,
    "firstName": "しゃいん",
    "lastName": "49号"
  }
]

3. 実装コード

注意)以下のコードは、paginationに関する部分のみに集中したコードです。DBもEFもクラスモデリングも無視でなるべく簡易な実装にしているので、クラス構成・メソッドの抽出等々は無視したコードです。

3.1. モデルクラス Employee。

// Models/Employee.cs
namespace PaginationExam.Models
{
  public class Employee
  {
    public int ID { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }
  }
}

3.2. Daoクラス

Employeeを取得するDaoクラス。(ダミーリストデータをメモリ内に作ってそれをLINQでpaginationして返すだけの実装)

// Dao/EmployeeDao.cs
using System.Linq;
using System.Collections.Generic;
using PaginationExam.Models;
using System;

namespace PaginationExam.Dao
{
  public class EmployeeDao
  {
    private List<Employee> _dummyEmployeeData = new List<Employee>();

    public EmployeeDao()
    {
      for (int i = 0; i < 100; i++)
      {
        this._dummyEmployeeData.Add(
          new Employee()
          {
            ID = i,
            FirstName = "しゃいん",
            LastName = i.ToString() + "号"
          });
      }
    }

    public (int totalItemCount, int lastPage, List<Employee>) GetEmployees(int page, int countPerPage)
    {
      int totalItemCount = 0;
      int lastPage = 0;
      List<Employee> employees = null;

      employees = this._dummyEmployeeData.Skip(countPerPage * (page - 1)).Take(countPerPage).ToList();
      totalItemCount = this._dummyEmployeeData.Count();

      lastPage = (int)Math.Floor((decimal)totalItemCount / countPerPage);
      if (totalItemCount % countPerPage > 0)
        lastPage++;

      return (totalItemCount, lastPage, employees);
    }
  }
}

3.3. コントローラクラス

Web API のコントローラクラス。 「GET api/Employee?page=xx」を受け付けます。
1ページ当たり5件は固定です。
pagination用のHTTP Response Headerは以下の3つを追加しています。

Links

以下の4つのリンク先を示します。
first - 1ページ目のURI prev - 前のページのURI
next - 次のページのURI
last - 最後のページのURI

X-TotalItemCount

「X-」なのでカスタムヘッダです。
Employeeの全件数を「X-TotalItemCount」に設定しています。

X-CurrentPage

現在のページを設定しています。

※「X-」カスタムヘッダは非推奨とされましたが、個人的にはその経緯から使ってOKじゃね?との認識。

// Controllers/EmployeeController.cs
using Microsoft.AspNetCore.Mvc;
using PaginationExam.Dao;
using PaginationExam.Models;
using System.Collections.Generic;

namespace PaginationExam.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  public class EmployeeController : Controller
  {
    // 1ページに5件
    const int CountPerPage = 5;

    [HttpGet]
    public IEnumerable<Employee> GetList([FromQuery] int page)
    {
      if (page < 1) // 1ページスタートなので、1未満は1に簡単に補正
        page = 1;

      // データ取得
      // 全件数、該当ページのEmployeeリスト
      var dao = new EmployeeDao();
      (int totalItemCount, int lastPage, List<Employee> employees) = 
        dao.GetEmployees(page, EmployeeController.CountPerPage);

      // Response Header追加
      this.Response.Headers.Add("Links", this.CreateLinksHeader("Employee", page, lastPage));
      this.Response.Headers.Add("X-TotalItemCount", totalItemCount.ToString());
      this.Response.Headers.Add("X-CurrentPage", page.ToString());

      // Body Jsonは本来のデータのみ
      return employees;
    }

    /// <summary>
    /// Pagination用のLinkヘッダ値を作成
    /// </summary>
    /// <param name="controller"></param>
    /// <param name="currentPage"></param>
    /// <param name="lastPage"></param>
    /// <returns></returns>
    protected string CreateLinksHeader(string controller, int currentPage, int lastPage)
    {
      List<string> links = new List<string>();

      links.Add(string.Format("<{0}>; rel=\"first\"", this.Url.Link("", new { Controller = controller, page = 1 })));
      if (currentPage > 1)
      {
        links.Add(string.Format("<{0}>; rel=\"prev\"", this.Url.Link("", new { Controller = controller, page = currentPage - 1 })));
      }
      if (currentPage < lastPage)
      {
        links.Add(string.Format("<{0}>; rel=\"next\"", this.Url.Link("", new { Controller = controller, page = currentPage + 1 })));
      }
      links.Add(string.Format("<{0}>; rel=\"last\"", this.Url.Link("", new { Controller = controller, page = lastPage })));

      return string.Join(", ", links);
    }
  }
}

4. 実行

実行します。
Postmanを起動して「GET https://localhost:44332/api/employee?page=10」をSendした結果は以下です。

BodyのJSONには本来のデータ(Employeeリスト)のみ。

f:id:daigo-knowlbo:20190106124027p:plain

Response Headerには Link / X-TotalItemCount / X-CurrentPage が返却されている。

f:id:daigo-knowlbo:20190106124106p:plain

5. まとめ

ASP.NET Core 2.2 でのpaginationについての一実装例でしたm( )m
ソースは以下に置いてあります。

github.com

あと、paginationに関する説明や検討事項は以下なんかが結構参考になると思います。

  1. 翻訳: WebAPI 設計のベストプラクティス - Qiita

  2. qiita.com

  3. RFC 5988 - Web Linking