BlazorからWebAPIを呼び出す

1. はじめに

まず、この記事はCliend side Blazorを前提とします。
Blazorアプリケーションは、つまりSPAなアプリですので、たいていの場合 データの表示・更新等のため に Web API呼び出し が必要になります。
今回は OpenWeatherMap が提供するWeb APIをBlazorから呼び出してみたいと思います。

サンプルコード: https://github.com/ryuichi111/BlazorExamples/tree/master/web_api_blazor

1.1. OpenWeatherMap API

OpenWeatherMap(https://openweathermap.org/)は気象予報情報を提供してくれるサイトで、APIも提供しています。
アカウントの登録を行いAPI Keyを取得すれば無料の「Freeプラン」でも、呼び出し回数制限や利用できるAPIの制限がありますが、十分に利用することができます。

https://openweathermap.org/price

Sign Upを行うとメールで API Key が送信されてきます。
また、サインインした状態で「https://home.openweathermap.org/」にアクセスし「API Keys」メニューをクリックする事でも API Key を確認することができます。

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

利用するAPI

Current weather data(https://openweathermap.org/current)を利用します。
Locationを指定することで気象予報を取得することができます。

上記リンク先にも説明がありますが、HTTP GETで以下のようなリクエストを行います。

http://api.openweathermap.org/data/2.5/forecast?q=Tokyo&units=metric&APPID=[API Key]

※ q: Locationを指定
  units=metric: 気温の単位を℃に設定するオプション
  APPID: API Key

すると以下のようなJSONデータが返却されます。

{
  "cod": "200",
  "message": 0.0063,
  "cnt": 40,
  "list": [
    {
      "dt": 1556949600,
      "main": {
        "temp": 25.31,
        "temp_min": 25.31,
        "temp_max": 25.99,
        "pressure": 1017.52,
        "sea_level": 1017.52,
        "grnd_level": 1013.68,
        "humidity": 22,
        "temp_kf": -0.68
      },
      "weather": [
        {
          "id": 801,
          "main": "Clouds",
          "description": "few clouds",
          "icon": "02d"
        }
      ],
      "clouds": {
        "all": 12
      },
      "wind": {
        "speed": 2.38,
        "deg": 145.83
      },
      "sys": {
        "pod": "d"
      },
      "dt_txt": "2019-05-04 06:00:00"
    },
    {
      "dt": 1556960400,
      "main": {
        "temp": 23.88,
        "temp_min": 23.88,
        "temp_max": 24.39,
        "pressure": 1018.58,
        "sea_level": 1018.58,
        "grnd_level": 1014.49,
        "humidity": 31,
        "temp_kf": -0.51
      },
      "weather": [
        {
          "id": 804,
          "main": "Clouds",
          "description": "overcast clouds",
          "icon": "04d"
        }
      ],
      "clouds": {
        "all": 99
      },
      "wind": {
        "speed": 4.72,
        "deg": 157.488
      },
      "sys": {
        "pod": "d"
      },
      "dt_txt": "2019-05-04 09:00:00"
    },
    ...省略
    
  ],
  "city": {
    "id": 1850147,
    "name": "Tokyo",
    "coord": {
      "lat": 35.6828,
      "lon": 139.759
    },
    "country": "JP",
    "population": 8336599
  }
}

2. サンプルを作成

早速サンプルプログラムを作成していきます。

完成イメージは以下の通りです。

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

  • 「Your API Key」テキストボックス:
    OpenWeatherMapにアクセスするためのAPI Keyを入力します。
  • 「Location」テキストボックス:
    気象情報を取得する地名を入力します。
  • 「検索」ボタン:
    OpenWeatherMap APIを呼び出し、結果を表形式で表示します。

2.1. プロジェクトを作成

テンプレートは Client-side Blazor(blazor)、プロジェクト名は web_api_blazor とします。

dotnet new blazor -o web_api_blazor

2.2. 余分な実装を削除

今回は Pages/index.razor に画面実装を行うこととします。
自動生成された以下のファイルは削除します。

  • Pages/Counter.razor
  • Pages/FetchData.razor

また Shared/NavMenu.razor も余分なメニューリンクを削除します。

# 修正後の Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
  <a class="navbar-brand" href="">web_api_blazor</a>
  <button class="navbar-toggler" onclick="@ToggleNavMenu">
    <span class="navbar-toggler-icon"></span>
  </button>
</div>

<div class="@NavMenuCssClass" onclick="@ToggleNavMenu">
  <ul class="nav flex-column">
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
        <span class="oi oi-home" aria-hidden="true"></span> Home
      </NavLink>
    </li>
  </ul>
</div>

@functions {
  bool collapseNavMenu = true;

  string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

  void ToggleNavMenu()
  {
    collapseNavMenu = !collapseNavMenu;
  }
}

2.3. モデルクラスの作成

OpenWeatherMapから返却されるJSONデータは前述で記載しましたが、これをC#で扱いやすくするために C#のPOCO としてモデルクラスを定義します。

(1) quicktype

JSONからC#クラスを生成する方法は色々あると思いますが、ここでは quicktype を使いたいと思います。

https://app.quicktype.io/

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

左上の「Name」には、モデルクラス名を設定します。ここでは WeatherResult としました。
そのすぐ下に JSON データをペーストします。
右端のLanguage設定パネルで、出力コード設定を行います。

  • Language: C#
    言語はもちろんC#に設定します。
  • Use T or List: List
    配列データを
    とするか List<> とするかの設定は List にします。
  • Outpupt features: Just Types
    Complete / Attributes Onlyという選択肢もありますが、これらは Newtonsoft.Json ベースのクラスを出力します。ここではプレーンなクラスが欲しかったので、Just Types を選択しました。

真ん中の背景が白い領域にコードが出力されます。
Copy Codeボタンをクリックすることで、これをクリップボードにコピーすることが出来ます。

(2) Models/WeatherResult.csを作成

web_api_blazorプロジェクトに Models/WeatherResult.cs を作成します。
quicktypeで作成したコードを利用しますが、そのままでは都合の悪い点がいくつかあるので、手動で修正します。

  • 必要なusing宣言を追加
  • namespace宣言を追加
  • 「public partial class List」を「public partial class WeatherSummary」に修正
  • enum定義を削除(合わせてそれらの型定義を使っているプロパティをstringに変更)
  • 「_」が省略されてしまったプロパティ名を修正(例:DtTxt → Dt_Txt)
  • DtJpプロパティを追加(日付情報Dt_TxtはUTCなのでTokyo時間に変換するgetterプロパティ)

以下が、修正を行った Models/WeatherResult.cs です。

# Models/WeatherResult.cs #

using System;
using System.Collections.Generic;

namespace web_api_blazor.Models {
    public partial class WeatherResult
  {
    public long Cod { get; set; }
    public double Message { get; set; }
    public long Cnt { get; set; }
    public List<WeatherSummary> List { get; set; }
    public City City { get; set; }
  }

  public partial class City
  {
    public long Id { get; set; }
    public string Name { get; set; }
    public Coord Coord { get; set; }
    public string Country { get; set; }
    public long Population { get; set; }
  }

  public partial class Coord
  {
    public double Lat { get; set; }
    public double Lon { get; set; }
  }

  public partial class WeatherSummary
  {
    public long Dt { get; set; }
    public MainClass Main { get; set; }
    public List<Weather> Weather { get; set; }
    public Clouds Clouds { get; set; }
    public Wind Wind { get; set; }
    public Sys Sys { get; set; }
    public DateTime Dt_Txt { get; set; }
    public DateTime DtJp { get { return this.Dt_Txt.AddHours(9); } }
    public Rain Rain { get; set; }
  }

  public partial class Clouds
  {
    public long All { get; set; }
  }

  public partial class MainClass
  {
    public double Temp { get; set; }
    public double TempMin { get; set; }
    public double TempMax { get; set; }
    public double Pressure { get; set; }
    public double SeaLevel { get; set; }
    public double GrndLevel { get; set; }
    public long Humidity { get; set; }
    public double TempKf { get; set; }
  }

  public partial class Rain
  {
    public double The3H { get; set; }
  }

  public partial class Sys
  {
    public string Pod { get; set; }
  }

  public partial class Weather
  {
    public long Id { get; set; }
    public string Main { get; set; }
    public string Description { get; set; }
    public string Icon { get; set; }
  }

  public partial class Wind
  {
    public double Speed { get; set; }
    public double Deg { get; set; }
  }

  //public enum Pod { D, N };

  //public enum Description { BrokenClouds, ClearSky, FewClouds, LightRain, OvercastClouds, ScatteredClouds };

  //public enum MainEnum { Clear, Clouds, Rain };
}

2.3. Pages/index.razor の実装

Pages/index.razorにメインの画面の実装を行います。

以下にペタッと完成した実装を貼り付けますが、コードの下でポイントの解説を行っています。

# すべての実装を行った Pages/index.razor #

@page "/"
@using web_api_blazor.Models
@inject HttpClient httpClient

<h1>OpenWeatherMap で お天気情報検索</h1>

<div class="form-group">
  <label for="text1">Your API Key:</label>
  <input type="text" class="form-control" bind="@AppID" />
</div>
<div class="form-group">
  <label for="passwd1">Location:</label>
  <input type="text" class="form-control" bind="@Location" />
</div>
<div class="form-group">
  <input type="button" onclick="@Search" value="検索" />
</div>
<hr />
@if( WeatherResult != null ) {
  <div>CityName: @WeatherResult.City.Name</div>
  <table class="table">
    <thead>
      <tr>
        <th scope="col">日時</th>
        <th scope="col">気温</th>
        <th scope="col">気圧(hPa)</th>
        <th scope="col">天気</th>
      </tr>
    </thead>
    <tbody>
    @foreach (var summary in WeatherResult.List)
    {
      <tr>
        <th scope="row">@summary.DtJp</th>
        <td>@summary.Main.Temp</td>
        <td>@summary.Main.Pressure</td>
        <td>
          @summary.Weather[0].Main
          (
          @summary.Weather[0].Main
          )
        </td>
      </tr>
    }
    </tbody>
  </table>  
}

@functions {
  private string AppID { get; set; }
  private string Location { get; set; } = "Tokyo";
  private WeatherResult WeatherResult { get; set; } = null;

  private async void Search() {
    string url = $"http://api.openweathermap.org/data/2.5/forecast?q={Location}&units=metric&APPID={AppID}";
    this.WeatherResult = await httpClient.GetJsonAsync<WeatherResult>(url);
    this.StateHasChanged();
  }
}

(1) @inject HttpClient httpClient

3行目で HttpClient をインジェクトしています。
このページでは、Web API呼び出しを行うために HttpClient をDIしています。
「HttpClient」はBlazorではデフォルトサービスであるため、Startup#ConfigureServices()で初期登録することなく利用することができます。
HttpClientのライフサイクルは Singleton です。

「@inject HttpClient httpClient」と宣言したため、@functionsコードブロックでは「httpClient」としてインスタンス参照することができます。

参考:https://docs.microsoft.com/en-us/aspnet/core/blazor/dependency-injection?view=aspnetcore-3.0

(2) API Key / Location 入力項目

<inout type="text>タグでAPI Key / Locationの入力項目を定義しています。
「UIによる入力 → AppID / Localtionプロパティ」というデータバインディングが必要なので「bind="@xxxx"」とします。つまり、two-wayのバインディング設定を行っています。

(3) Search() / httpClient.GetJsonAsync<T>()

検索ボタンのクリック時にSearch()メソッドが呼び出されます。
UIによる入力項目を利用し、OpenWeatherMap APIを呼び出すURLを作成します。

string url = $"http://api.openweathermap.org/data/2.5/forecast?q={Location}&units=metric&APPID={AppID}";

HTTPリクエストの実行には httpClient を利用し、GetJsonAsync<T>()メソッドを利用します。

this.WeatherResult = await httpClient.GetJsonAsync<WeatherResult>(url);

JSON文字列をそのまま受け取りたい場合は httpClient.GetStringAsync(url) を呼び出します。

GetJsonAsync<T>() は、内部的に GetStringAsync() を呼び出し JsonUtil.Deserialize(json) を行っています。

https://github.com/aspnet/Blazor/blob/8c9910743959b16d7ff3b2fbb1014fe000a716e4/src/Microsoft.AspNetCore.Blazor/Json/HttpClientJsonExtensions.cs#L23

(4) StateHasChanged()

コンポーネントの状態変更をマニュアル通知し、画面の再レンダリングを行います。

(5) レンダリング定義

「div / table / tr / td 等」+「@xx Razor構文」で、WeatherResultを表形式でレンダリングしています。
CSSはbootstrapのもので、C#コードはRazor構文で記述します。

2.4. 実行

web_api_blazorディレクトリで dotnet run コマンドを実行して、ブラウザで http://localhost:5000 にアクセスします。

3. まとめ

ということで、BlazorからWeb APIの呼び出しを試してみました。
デフォルトサービスとして用意されている HttpClient を使う、という事のみが重要なポイントでした。
HttpClientクラスは、C#経験者なら馴染み深いものなので、これは問題ないですね。
モデルクラスの用意に関しては、今回はJSONデータのみが手に入る状態からC#コードの生成を行うため、quicktypeを利用しました。この辺りはBlazorに限ったことではなく、必要に応じてケースバイケースでの用意になるかと思います。
また、API Keyを画面で入力してBlazorクライアントから直接OpenWeatherMap APIを呼び出す実装としました。これについては、自前のWeb APIでラップする等のアーキテクチャはサンプルという事で無視しておりますのであしからずm( )m