【C#】HttpClientでWeb APIクライアントを作る　(チュートリアル)

# はじめに
HttpClientの使ってHTTP GET/PUT/POST/DELETEのWeb APIをコールする初歩的なアプリを作成した。
作成手順を紹介する。
チュートリアル的な内容

# 記事投稿の背景
HttpClientを使うのが、初めてだった。チュートリアルやサンプルコードを検索したが、私の期待に沿うものがなかった（検索結果は下記表の通り）。

| No | 検索結果 | 詳細 |
|:-|:-|:-|
| 1 |[公式ドキュメント(リンク)][1]にサンプルコードがない。|HTTPの基本的なメソッド(GET/PUT/POST/DELETE)を呼ぶメソッドのサンプルがない。|
| 2  |サーバアプリのサンプルが過剰だった|クライアントと通信するサーバが必要になる。[非公式のチュートリアル(リンク)][2]を調べると、サーバアプリはDBアクセスを伴うコード例が多い。クライントからWeb APIをコールする方法を学ぶことが目的なので、DBアクセスなどは不要。準備するのもコストなので、サーバ側は極力シンプルにしたい。|
| 3  |[非公式のチュートリアル(リンク)][3]がGET/PUTのみで、情報不足。|サーバを準備しなくても、動作確認できるチュートリアルもあったが、サンプルがGET/PUTのみ。C#9.0の記法が混ざっている点も厄介。|

# 想定読者
* HttpClientを初めて使う人


# 開発環境
Visual Studio 2019(ASP.NETの開発設定済みのもの)
　未設定の場合は[こちら][4]を参照して下さい
フレーワーク：.NET Core2.1

# 作成手順
(1)ASP.NET Coreで単純なサーバアプリを作る。
　自動生成コードを利用するため、特別な知識は不要。
(2)Web APIクライアントアプリを作成する。
(3)Visual Studioを複数立ち上げて、サーバとクライアントをデバック実行して動作確認する。

## (1)ASP.NET Coreで単純なサーバアプリを作る

### ASP.NET Coreでプロジェクトを作成する。
ソリューションの名前は「SimpleWebServer」にした。
※名前は任意
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/b583a3dc-299e-cf60-7292-03ea44a711be.png)

学習目的で自分PCで動作確認するだけなので、<b>HTTPS用の構成のチェックを外した</b>。
プロジェクトを生成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/c399719d-c5b3-9c24-6b7c-5c52509eec95.png)

### 自動生成コードを確認する。

生成されたプロジェクトのControllersフォルダ直下に<b>ValuesController.cs</b>が存在することを確認する。
実装内容は下記の通り。

~~~C#:ValuesController.cs
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SimpleWebServer.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get( int id )
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post( [FromBody] string value )
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put( int id, [FromBody] string value )
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete( int id )
        {
        }
    }
}
~~~

#### 自動生成コードのWebAPI一覧

| HTTPメソッド | URL | 対応関数と補足説明 |
|:-|:-|:-|
|GET   |http://localhost:19691/api/values   |public ActionResult<IEnumerable<string>> Get()  |
|GET   |http://localhost:19691/api/values//{id}   |public ActionResult<string> Get( int id )<br>idの有無で引、呼び出される関数か識別される。|
|POST   |http://localhost:19691/api/values   |public void Post( [FromBody] string value )<br>本記事では、HTTPのbodyの書式（Content-Typeヘッダ）は「application/json（JSON形式）」で渡す。<br>valueにJSON文字列が格納される。   |
|PUT   |http://localhost:19691/api/values/{id}   |public void Put( int id, [FromBody] string value )<br> 本記事では、HTTPのbodyの書式（Content-Typeヘッダ）は「application/json（JSON形式）」で渡す。<br>valueにJSON文字列が格納される。  |
|DELETE   |http://localhost:19691/api/values/{id}   |public void Delete( int id )   |

### ビルドできることを確認する。

### デバック時の設定を確認する。
生成したプロジェクトを選択する。
Visual Studio の「プロジェクトのタブ」→「xxxxxのプロパティ(※)」を押下。

![プロジェクトプロパティ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/7ec4f32d-19c1-ddf4-28e3-d772e3d3cb05.png)

デバックのタブを押下する。
[ブラウザ起動]のチェックを外す。

![ブラウザ起動解除.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/f7324379-8812-fcbd-3d16-8078f7881d6e.png)

スクロールして、URLを確認する。クライアントでURLを入れるときに使用する。
![アプリのURL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/91f5a0a7-56b5-99a4-0784-69a4cbe12214.png)

## (2)単純なWebAPIクライアントアプリを作る

### コンソールアプリケーションプロジェクトを作成する。
今回は.NET Core2.1を使用した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/4ea2957f-1689-bead-a716-293b260a9993.png)

この例では、JSONシリアライザにJSON.NETを利用する。Nugetパッケージからインストールすること。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/02896396-4e97-6234-d9a9-7b447dbcb4a1.png)

### クライアントの実装
URLはご自身のサーバの設定に合わせて、書き換えて下さい。

~~~C#:Program.cs
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace SimpleWebApiClient
{
    class Program
    {

        static async Task Main( string[] args )
        {
            var valueApi = new WebApiClient();
            await valueApi.GetAsync();
            await valueApi.PutAsync();
            await valueApi.PostAsync();
            await valueApi.DeleteAsync();
        }
    }

    /// <summary>
    /// Web APIクライアント
    /// </summary>
    public class WebApiClient
    {
        /// <summary>
        /// HTTPクライアント
        /// </summary>
        /// <remarks>
        /// IDisposableを継承しているが、usingで囲わないこと
        /// 要求ごとにインスタンス化すると、ソケットが枯渇する懸念があるため、避けたほうが良い。
        /// 参考
        ///  下記の[HttpClientを作成して初期化する]を参照
        ///  https://docs.microsoft.com/ja-jp/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client
        ///  InfoQでも言及されている
        ///  https://www.infoq.com/jp/news/2016/09/HttpClient/
        /// </remarks>
        private static HttpClient Client = new HttpClient();

        /// <summary>
        /// GETメソッドをコールする
        /// </summary>
        /// <returns></returns>
        public async Task GetAsync()
        {
            try
            {
                //HTTP　GET要求を送信する
                Console.WriteLine("Call GET api/values");
                var resource = await Client.GetAsync("http://localhost:19691/api/values");
                //ステータス異常の時に例外をスルーする
                resource.EnsureSuccessStatusCode();
                //HTTP応答を取得して表示する
                var responseBody = await resource.Content.ReadAsStringAsync();
                Console.WriteLine(responseBody);
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            Console.WriteLine("End");
        }

        /// <summary>
        /// PUTメソッドをコールする
        /// </summary>
        /// <returns></returns>
        public async Task PutAsync()
        {
            try
            {
                //HTTP　PUT要求を送信する
                Console.WriteLine("Call PUT api/values/5");
                var testInput = "TestMessage";
                var jsonString = JsonConvert.SerializeObject(testInput);
                var data = new StringContent(jsonString, Encoding.UTF8, mediaType: "application/json");
                var response = await Client.PutAsync("http://localhost:19691/api/values/5", data);
                response.EnsureSuccessStatusCode();
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            Console.WriteLine("End");
        }

        /// <summary>
        /// POSTメソッドをコールする
        /// </summary>
        /// <returns></returns>
        public async Task PostAsync()
        {
            try
            {
                //HTTP　POST要求を送信する
                Console.WriteLine("Call POST api/values");
                var testInput = "PostMessage";
                var jsonString = JsonConvert.SerializeObject(testInput);
                var data = new StringContent(jsonString, Encoding.UTF8, mediaType: "application/json");
                var response = await Client.PostAsync("http://localhost:19691/api/values", data);
                response.EnsureSuccessStatusCode();
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            Console.WriteLine("End");
        }
        /// <summary>
        /// DELETEメソッドをコールする
        /// </summary>
        /// <returns></returns>
        public async Task DeleteAsync()
        {
            try
            {
                //HTTP　DELETE要求を送信する
                Console.WriteLine("Call DELETE api/values/5");
                var response = await Client.DeleteAsync("http://localhost:19691/api/values/5");
                response.EnsureSuccessStatusCode();
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            Console.WriteLine("End");
        }
    }
}
~~~


## サーバとクライアントアプリをデバック実行する
Visual Studioのソリューションを2つ立ち上げた状態で下記手順を実施する（サーバ用、クライアント用の2つ）。

### (3)動作確認

#### サーバをデバック実行する。
 ①サーバを先に立ち上げる。
 ②下記メソッドにブレークポイントを貼る。
 ③デバック実行する。

~~~C#
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get(){//略}

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put( int id, [FromBody] string value ){}

        // POST api/values
        [HttpPost]
        public void Post( [FromBody] string value ){}
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete( int id ){}
~~~~

### クライアントを実行する。

#### GETメソッドをコール
サーバ側のGETメソッドが呼ばれていたら、OKです。
サーバ側で返した下記文字列（※）はHTTP応答に付与されます。
※"value1", "value2"

クライアント側でHTTP応答を読み取ると、サーバで返した文字列が表示される。

![GETメソッド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/ac5625d0-d9d1-7a54-1945-50927389d06f.gif)

#### PUTメソッドをコール
サーバが受け取った値がクライアントの送信内容に一致するか確認する。

![Putメソッド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/550d0d74-b678-a714-7422-4e05930b0f9d.gif)

#### POSTメソッドをコール
サーバが受け取った値がクライアントの送信内容に一致するか確認する。

![POSTメソッド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/95b48154-cf7c-d556-3f0c-a0ce84140832.gif)

#### DELETEメソッドをコール
URIに指定した値(5)をサーバが受け取れば、OK。

![DELETEメソッド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/c993a436-f5bf-e2a3-174d-815c5e5d14d7.gif)

# まとめ
Web APIを呼び出すサンプル実装を示した（GET/PUT/POST/DELETE）。
今回は勉強のため、HttpClientに標準で定義されたメソッドのみを利用した。

記事では触れていないが、実装をより良くする手法として下記2つがある'次にクライアントを作るときは、組み込みたいと思う)。
(1)Microsoftが提供しているパッケージを使う。
例えば、Microsoft.Asp.Net.WebApi.Clientなど。
HttpClentの拡張メソッドが定義されているため、もっと楽に実装できそう。
[参考][5]

(2)IHttpClientFactoryを使う。[こちら][8]に解説記事があるため、ここでは触れない。


# 参考

| No | リンク | 備考 |
|:-|:-|:-|
|1   |[HTTPClientクラス][1]   |Microsoft公式ドキュメント。   |
|2   |[Tutorial Teacher][2]|非公式のチュートリアル(英語)。HttpClientの使用経験が全くない状態だと、情報過多だった。   |
|3   |[ZetCode C# HttpClient][3]   |非公式のC#　HttpClientのチュートリアル(英語)。<br>サーバが準備されており、サンプルソースを書いて動かせる点が良かった。<br>GET/PUTのみなのが、物足りない。C#9.0の記法が混ざっていることも厄介だった。|
|4   |[ASP.NET Coreを利用するためのVisual Studio 2019のインストール方法][4]   |QiitaにあったASP.NET Coreのインストール手順の記事。   |
|5   |[.NET クライアントから Web API を呼び出す][5]  |Microsoftの公式ドキュメント ASP.NET Web APIガイダンス。Microsoft.Asp.Net.WebApi.Clientパッケージを用いたWeb Apiクライアントの作成方法が記載されている。本記事の次のステップとして、この実装を真似ると良いと思う。|
|6  |[開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント][6]   |InfoQの記事。HttpClientを使う上での注意点について述べられている。   |
|7   |[HttpClientをusingで囲わないでください][7]   |Qiitaの記事。タイトル通りの内容。   |
|8   |[IHttpClientFactory を使って今はこれ一択と思った話][8]   |IHttpFactoryを紹介したQiitaの記事。|
|9   |[C# 今更ですが、HttpClientを使う][9]   |HttpClientの使い方をまとめたQiitaの記事。|
|10  |[HttpClientのBaseAddressを使う方法][10]|HttpClientのBaseAddressプロパティに関する記事|

[1]:https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=net-5.0
[2]:https://www.tutorialsteacher.com/webapi/consuming-web-api-in-dotnet-using-httpclient
[3]:https://zetcode.com/csharp/httpclient/
[4]:https://qiita.com/kami_teru/items/49d95194bcb4836b591e
[5]:https://docs.microsoft.com/ja-jp/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client
[6]:https://www.infoq.com/jp/news/2016/09/HttpClient/
[7]:https://qiita.com/superriver/items/91781bca04a76aec7dc0
[8]:https://qiita.com/TsuyoshiUshio@github/items/7092fbc510772ce5db63
[9]:https://qiita.com/rawr/items/f78a3830d894042f891b
[10]:https://sqlazure.jp/r/c/546/