C# - Qiita API v2 でページビューの一覧取得

## はじめに

Zenn では、統計ダッシュボードで、記事ごとのページビューを一覧表示できます。
Qiita にはこのような機能がないため、Qiita API v2 を用いて情報を取得する方法を説明します。

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、基本的に C# 7.3 機能範囲で記述しています。
:::

### 参考情報
下記情報を参考にさせて頂きました。

- [Qiita API v2 の概要（非公式）](https://qiita.com/tag1216/items/b0b90e30c7e581aa2b00)
- [自分の書いたQiita記事の閲覧数を一覧でサクッと確認する手順作ってみました](https://qiita.com/EIJI_at_FujiwaaaLand/items/b11423b79876b325836e)



## 前準備

### アクセストークン取得

記事に関する情報取得は、/api/v2/authenticated_user/items を利用します。

https://qiita.com/api/v2/docs#get-apiv2authenticated_useritems

上記を利用する場合、事前にアクセストークン取得が必要となります。
Qiita にログインして、右上 アカウントのアイコンから「設定」を選択します。

![AccessToken-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/01e03e02-02e1-43dd-a498-5b20c9acdbdb.png)

設定画面で「アプリケーション」「個人用アクセストークン - 新しくトークンを発行する」を選択します。

![AccessToken-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/894bc4e4-22c7-4aee-96bd-348dbcd79cf2.png)

「アクセストークンの説明」を入力、スコープ「read_qiita」として「発行する」を選択します。

![AccessToken-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/441db724-a1d1-4784-b524-915ec3b9b4e0.png)

アクセストークンが表示されるので「アクセストークンをコピー」します。

![AccessToken-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/097de821-3612-4458-bc9e-820b4d5cfc40.png)

### JSON デシリアライズ

JSON デシリアライズは、現在 Microsoft が推奨している System.Text.Json を利用します。
.NET Framework の場合、System.Text.Json は標準提供されていないので、[NuGet Gallery | System.Text.Json](https://www.nuget.org/packages/System.Text.json) 導入が必要です。

```
PM> NuGet\Install-Package System.Text.Json
```

### HttpClient

[GROWI - Ubuntu 24.04 への導入と Windows C# クライアント](https://qiita.com/chai0917/items/8644b50d955a5cb1acc9#httpclient) と同様に、HttpClient を IHttpClientFactory で利用します。
DependencyInjection、IHttpClientFactory を利用するので、[NuGet Gallery | Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/microsoft.extensions.dependencyinjection/) と [NuGet Gallery | Microsoft.Extensions.Http](https://www.nuget.org/packages/microsoft.extensions.http/) を導入します。

```
PM> NuGet\Install-Package Microsoft.Extensions.DependencyInjection
PM> NuGet\Install-Package Microsoft.Extensions.Http
```

## サンプルコード

.NET Framework 4.8 と .NET 8 では、軽微な差異があるので、まず .NET 8 のサンプルコードを記載して、次に .NET Framework 4.8 の差分を記載します。

### .NET 8

まずは、ServiceCollection に対して、QiitaClient という名称の HttpClient を登録した ServiceProvider を用意します。

```CSharp
private static IServiceProvider? MyServiceProvider = null;
private const string AccessToken = "<事前準備で取得したアクセストークン>";  // TODO
```
```CSharp
var serviceCollection = new ServiceCollection();
serviceCollection.AddHttpClient("QiitaClient", client =>
{
  client.BaseAddress = new Uri("https://qiita.com");
  client.DefaultRequestHeaders.Authorization = 
                       new AuthenticationHeaderValue("Bearer", AccessToken);
  client.DefaultRequestHeaders.Add("Accept", "application/json");
});
MyServiceProvider = serviceCollection.BuildServiceProvider();
```

/api/v2/authenticated_user/items クライアント処理を用意します。

```CSharp
// Qiita API v2 - /api/v2/authenticated_user/items
private async Task<List<ResponsePage>?> QiitaGetPageList()
{
  // IHttpClientFactory で QiitaClient という名前の HttpClient 取得
  var factory = MyServiceProvider?.GetService<IHttpClientFactory>();
  var httpClient = factory?.CreateClient("QiitaClient");
  if (httpClient == null)
  {
    // エラー発生 - TODO
    return null;
  }

  string baseUrl = "/api/v2/authenticated_user/items";
  var lstPages = new List<ResponsePage>();
  int totalCount = 0;  // 全件数
  int limit = 20;      // 1回の取得件数
  int page = 1;        // ループ回数（1～）

  // 1回の処理は limit 件のページ取得なので、ループ処理とする
  while (true)
  {
    string targetUrl = baseUrl + $"?page={page}&per_page={limit}";
    using (var request = new HttpRequestMessage(HttpMethod.Get, targetUrl))
    using (var response = await httpClient.SendAsync(request))
    {
      if (response?.IsSuccessStatusCode == true)
      {
        var content = response.Content.ReadAsStringAsync().Result;
        var obj = System.Text.Json.JsonSerializer
                     .Deserialize<ResponsePage[]>(content);
        if (obj == null)
        {
          // エラー発生 - TODO
          return null;
        }
        if (totalCount == 0) // 初回
        {
          if (response.Headers.Contains("Total-Count") 
           && int.TryParse(response.Headers.GetValues("Total-Count").First(),
                                                      out int count))
          {
            totalCount = count;
          }
        }
        if (obj.Length > 0)
        {
          lstPages.AddRange(obj);
        }
      }
      else
      {
        // エラー発生 - TODO
        return null;
      }
    }
    // 残りがあるか？
    if (totalCount <= (page++ * limit))
    {
      break;
    }
  }
  return lstPages;
}
// JSON 用クラス
public class ResponsePage
{
  // 利用する項目のみ定義
  public required string title { get; set; }   // タイトル
  public DateTime created_at { get; set; }     // 作成日時
  public DateTime updated_at { get; set; }     // 更新日時
  public int? page_views_count { get; set; }   // ページビュー
  public int? likes_count { get; set; }        // Like数
  public int? stocks_count { get; set; }       // Skocks数
}
```

上記処理を以下のように呼び出します。

```CSharp
// Qiita API v2 - /api/v2/authenticated_user/items
var lstPages = await QiitaGetPageList();
// TODO - CSV出力など...
```

### .NET Framework 4.8

.NET Framework 4.8 では、下記差分があるので、サンプルコードの一部を書き換えます。
- null 許容参照型の明示は不要
  - MyServiceProvider、QiitaGetPageList 戻り値、および、JSON 用クラス

```CSharp
private static IServiceProvider MyServiceProvider = null;
```
```CSharp
// Qiita API v2 - /api/v2/authenticated_user/items
private async Task<List<ResponsePage>> QiitaGetPageList()
{
＜中略＞
}
// JSON 用クラス
public class ResponsePage
{
  // 利用する項目のみ定義
  public string title { get; set; }            // タイトル
  public DateTime created_at { get; set; }     // 作成日時
  public DateTime updated_at { get; set; }     // 更新日時
  public int? page_views_count { get; set; }   // ページビュー
  public int? likes_count { get; set; }        // Like数
  public int? stocks_count { get; set; }       // Skocks数
}
```