アプリ開発ときどきアウトドア

主にJavaを使ったアプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

1. システムエンジニアリング ASP.NET Core 実装技術

ASP.NET Core: IHttpClientFactoryのサンプル

投稿日:2020年4月18日 更新日:

前回の記事でIHttpClientFactoryの使用方法を説明しました。
ここでは、サンプルを使用した具体的な使用方法を説明します。

前提

  • マイクロソフトが推奨するIHttpClientFactoryを使用して、HttpClientクライアントを取得する前提です。
  • HttpClientの使い方は何種類かありますが、ここでは既定のクライアント、名前付きクライアントを前提とします。
    実際の業務では、名前付き/型付きクライアントを使う場合が多いと思われますが、HttpClientとしての使用法は大きく変わらないため。
  • サンプルはWindows10 + Visual Studio 2019(ASP.NET Core 3.1)環境で確認しています。

HttpClientの基本的な使い方

サービスにクライアントを登録・構成し、
必要なタイミングでクライアントを取得してHTTP要求を実行する流れになります。

サービスへのクライアントの追加と構成

次のようにStartup.csでAddHttpClient()を使ってクライアントを登録します。
下記の例のようにクライアントの構成を行う他に、独自に要求/応答をカスタマイズするためのハンドラやエラー処理をカスタマイズするためのPollyポリシーを設定することもできます。

public void ConfigureServices(IServiceCollection services)
{
...
    services.AddHttpClient();               // 既定のクライアントを登録する場合
    services.AddHttpClient("name");         // 名前付きクライアントを登録する場合
    services.AddHttpClient<MyHttpClient>(); // 型付きクライアントを登録する場合

    // 名前付きクライアントを登録・構成する場合
    services.AddHttpClient("name2", options => {
        options.BaseAddress = new Uri("https://...");
        options.DefaultRequestHeaders.Add("key", "value");
    });

HTTP要求の実行

自由度の高いHTTP要求を作成できるSendAsync()を使うか、
より簡潔にHTTP要求を作成できるGetAsync()/PostAsync()等の便利なメソッドを使用する方法があります。

汎用的な方法: SendAsync()の使用

HTTP要求を表現するHttpRequestMessageオブジェクトを作成し、HTTPメソッドやURIの指定、必要に応じてHTTPヘッダやコンテンツ(ボディ)を指定します。
このオブジェクトを引数として、HttpClientのSendAsync()を実行します。HTTP応答はHttpResponseMessageに格納され、ステータスコードの確認、必要に応じてHTTPヘッダやコンテンツを取得できます。

public class XXXController : Controller
{
    private readonly IHttpClientFactory _factory;

    public XXXController(IHttpClientFactory factory)
    {
        _factory = factory;
    }

    public async Task<IActionResult> TestPost()
    {
        // HTTP要求の作成
        var request = new HttpRequestMessage(HttpMethod.Post, "https://...");
        request.Headers.Add("KEY", "value");
        request.Content = new StringContent("hello world!");

        // HTTP要求の送信
        HttpClient client = _factory.CreateClient();
        HttpResponseMessage response = await client.SendAsync(request);
        if (response.IsSuccessStatusCode)
        {
	    // 正常時の処理
	    string body = await response.Content.ReadAsStringAsync();
            ...
        }else{
            // エラー時の処理
        }
  • 既定のクライアントや名前付きクライアントの場合、コンストラクタで定義したIHttpClientFactoryからクライアントを取得する必要があります。(コンストラクタでIHttpClientFactoryを引数宣言するとDependency Injectionの仕組みでインスタンスが設定されます。)
  • HttpRequestMessageでは、HeadersプロパティでHTTPヘッダの編集、Contentプロパティでコンテンツ(HTTPボディ相当)を設定ができます。
  • ContentプロパティはHttpContent型となっており、このクラスを継承した次のコンテンツを指定できます。詳細はリファレンスを参照のこと。
    コンテンツ(クラス) 説明
    StringContent テキスト形式のコンテンツ
    Content-Typeの既定は”text/plain; charset=utf-8″であり、コンストラクタの引数やHeadersプロパティから指定可。
    StreamContent
    ByteArrayContent
    ストリーム型・バイト配列型のコンテンツ
    既定のContent-Typeが指定されないので、Headers.ContentTypeプロパティやHttpRequestMessageで指定することになります。
    FormUrlEncodedContent フォーム形式のコンテンツ
    input要素で入力した想定の形式です。
    Content-Typeは”application/x-www-form-urlencoded”になります。
    MultipartFormDataContent マルチパートフォーム形式のコンテンツ
    input要素で入力した想定の形式で、ファイルアップロードの場合に使用する。
    Content-Typeは”multipart/form-data; boundary=…”になります。
    MultipartContent 任意のマルチパート形式のコンテンツ
    Webアプリでマルチパート形式を使用する場合、上記の”multipart/form-data”になるので、このコンテンツを使用することはないと思います。MultipartFormDataContentの基底クラスです。

簡潔な方法: GetAync(), PostAsync()等の使用

StringContent等のコンテンツを使ってHTTP要求を実行できます。
HttpRequestMessageオブジェクトを作成する必要がなく、前述の汎用的な方法より簡潔に実装できます。
この方法では、HTTPメソッドに対応するGetAsync(), PostAsync(), PutAsync(), DeleteAsync()が提供されています。これらのメソッドに対して、URIとコンテンツを引数してHTTP要求を実行します。

public class XXXController : Controller
{
    private readonly IHttpClientFactory _factory;

    public XXXController(IHttpClientFactory factory)
    {
        _factory = factory;
    }

    public async Task<IActionResult> TestPostSimple()
    {
        // HTTP要求(HttpContent)の作成
        var content = new StringContent("hello world!");
        content.Headers.Add("KEY", "value");

        // HTTP要求の送信
        HttpClient client = _factory.CreateClient();
        HttpResponseMessage response = await client.PostAsync("https://...", content);
        if (response.IsSuccessStatusCode)
        {
	    // 正常時の処理
	    string body = await response.Content.ReadAsStringAsync();
            ...
        }else{
            // エラー時の処理
        }

参考

  • 常時付与するHTTPヘッダがある場合、Startup時のAddHttpClientオプション(HttpClient.DefaultRequestHeaders)で指定することをお薦めします。
  • 業務アプリの開発では、開発環境と本番環境等のように実行環境に応じて、ベースURIを切り替えられるようにしたい場合があります。Startup時のAddHttpClientオプション(HttpClient.BaseAddress)で”https://xxxx/”等のベースのURIを指定し、個別のHttpClientの呼び出しでは”/api/xxx”等の相対パスを指定するような実装をお薦めします。

サンプル

ここではPOST/GETを使った基本的なサンプルを説明します。
完全なソースはこちらをご覧ください。(このサンプルでは、ASP.NET Core MVCのコントローラからIHttpClientFactoryを使って、ローカルのAPIコントローラにHTTP要求を発行しています。)

また、各サンプルでは、次のように名前付きクライアントとして登録した”basic”を使用しています。

public void ConfigureServices(IServiceCollection services)
{
...
    services.AddHttpClient("basic", options => {
        options.BaseAddress = new Uri("https://localhost:44399");
        options.DefaultRequestHeaders.Add("X-ACCESS-KEY", "secret");
        options.DefaultRequestHeaders.Accept.Clear();
        options.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    });

クエリ文字列を指定してGET実行

クエリ文字列の作成は、.NET Core 3系から標準で含まれるWebUtilitiesのQueryHelpersを使用できます。
HTTP要求の送信はHttpClientのGetAsync()で行います。DELETEを実行する場合はDeleteAsync()を使用します。

public async Task<IActionResult> PostBasicGet()
{
    var queries = new Dictionary<string, string>();
    queries.Add("arg1", "abcxyz!#$%&_=-003");
    queries.Add("arg2", "あいうえお");
    string uri = QueryHelpers.AddQueryString("/api/SampleApi", queries);

    HttpClient client = _factory.CreateClient("basic");
    HttpResponseMessage response = await client.GetAsync(uri);
    if( response.IsSuccessStatusCode)
    {
        // ...
    }

生成されるHTTP要求は次のようになります。
(ログに出力されるURLを見るとarg1はURLエンコードあり、arg2はURLエンコードなしに見えますが、実際にはどちらもURLエンコードされていました。)

GET /api/SampleApi?arg1=abcxyz!%23$%25%26_%3D-003&arg2=%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A ...
Host: ...
X-ACCESS-KEY: secret
Accept: application/json
Request-Id: ...

HTTPヘッダを指定してGET実行

上記の方法だとクライアント登録時の構成でしかHTTPヘッダを指定できません。
このような場合は汎用的な方法を使います。

public async Task<IActionResult> PostGenericGet()
{
    // HTTP要求の作成
    var request = new HttpRequestMessage(HttpMethod.Get, "/api/SampleApi");
    request.Headers.Add("X-ARG1", "abcxyz!#$%&_=-003");
    request.Headers.Add("X-ARG2", "あいうえお");

    // HTTP要求の送信
    HttpClient client = _factory.CreateClient("basic");
    HttpResponseMessage response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        // ...
    }

生成されるHTTP要求は次のようになります。

GET /api/SampleApi HTTP/1.1
Host: ...
X-ARG1: abcxyz!#$%&_=-003
X-ACCESS-KEY: secret
Accept: application/json
Request-Id: ...

フォーム形式によるPOST実行

FormUrlEncodedContentオブジェクトにキーと値を格納し、HTTP要求を発行します。
Content-Typeは”application/x-www-form-urlencoded”になります。

public async Task<IActionResult> PostBasicPost()
{
    var values = new Dictionary<string, string>();
    values.Add("arg1", "abcxyz!#$%&_=-003");
    values.Add("arg2", "あいうえお");
    var content = new FormUrlEncodedContent(values);

    HttpClient client = _factory.CreateClient("basic");
    HttpResponseMessage response = 
        await client.PostAsync("/api/SampleApi/post-only", content);
    if (response.IsSuccessStatusCode)
    {
        // ...
    }

生成されるHTTP要求は次のようになります。

POST /api/SampleApi/post-only HTTP/1.1
Host: ...
X-ACCESS-KEY: secret
Accept: application/json
Request-Id: ...
Content-Type: application/x-www-form-urlencoded
Content-Length: 85

arg1=abcxyz%21%23%24%25%26_%3D-003&arg2=%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A

マルチパートフォーム形式によるPOST実行

MultipartFormDataContentオブジェクトにキーと値を格納し、HTTP要求を発行します。
値として、アップロードするファイル内容を表現するストリームやバイト配列を指定できます。
Content-Typeは”multipart/form-data”になります。

public async Task<IActionResult> PostBasicPostMultipart()
{
    // サンプルとして添付するストリーム
    var bin = new byte[] {1, 2, 3, 4, 5, 6, 7, 8};
    var stream = new MemoryStream(bin);

    var content = new MultipartFormDataContent();

    // マルチパート: arg1
    content.Add(new StringContent("abcxyz!#$%&_=-003"), "arg1");

    // マルチパート: arg2
    content.Add(new StringContent("あいうえお"), "arg2");

    // マルチパート: arg3
    var streamContent = new StreamContent(stream);
    streamContent.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    streamContent.Headers.ContentDisposition =
        new ContentDispositionHeaderValue("attachment")
        {
            FileNameStar = "添付ファイル1.dat", // エンコードしたファイル名(RFC5987)
            FileName = "attached-file1.dat" // ファイル名(FileNameStarが優先)
        };
    content.Add(streamContent, "arg3");

    HttpClient client = _factory.CreateClient("basic");
    HttpResponseMessage response =
        await client.PostAsync("/api/SampleApi/post-only", content);
    if (response.IsSuccessStatusCode)
    {
        // ...
    }

生成されるHTTP要求は次のようになります。

POST /api/SampleApi/post-only HTTP/1.1
Host: ...
X-ACCESS-KEY: secret
Accept: application/json
Request-Id: ...
Content-Type: multipart/form-data; boundary="c520cbd6-7865-4910-8495-82f139e207c2"
Content-Length: 562

--c520cbd6-7865-4910-8495-82f139e207c2
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=arg1

abcxyz!#$%&_=-003
--c520cbd6-7865-4910-8495-82f139e207c2
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=arg2

あいうえお
--c520cbd6-7865-4910-8495-82f139e207c2
Content-Type: application/octet-stream
Content-Disposition: attachment; filename*=utf-8''%E6%B7...%83%AB1.dat; filename=attached-file1.dat

(バイナリデータ)
--c520cbd6-7865-4910-8495-82f139e207c2--

プロキシの使用

HttpClientが実際の通信を行う際、内部でメッセージハンドラを使用しています。
.NET Core 2.1以降では、既定のメッセージハンドラとしてSocketsHttpHandlerが使用されており、このプロパティでプロキシを指定できます。
詳細はSocketsHttpHandlerのリファレンスを参照のことですが、プロキシ以外にもCookieやコネクションタイムアウト等の細かい設定が可能です。.NET Core 2.1より前の既定のメッセージハンドラはHttpClientHandlerでした。HttpClientHandlerに比べ、SocketsHttpHandlerはより高性能でプラットフォーム非依存とのことです。

public void ConfigureServices(IServiceCollection services)
{

    services.AddHttpClient("proxy")
        .ConfigurePrimaryHttpMessageHandler(() =>
        new SocketsHttpHandler
        {
            UseProxy = true,
            Proxy = new WebProxy("localhost", 8888)
        }
    );
public async Task<IActionResult> DoGetViaProxy()
{
    HttpClient client = _factory.CreateClient("proxy");
    HttpResponseMessage response = await client.GetAsync("https://www.yahoo.co.jp");
    if (response.IsSuccessStatusCode)
    {
        // ...
    }

ローカルで検証用に構築したプロキシサーバ(Apache HTTPサーバ)を使って動作を確認します。
このプロキシサーバの構築についてはこちらをご覧ください。

出だしだけですが、生成されるHTTP要求の例は次のようになります。

CONNECT www.yahoo.co.jp:443 HTTP/1.1
Host: www.yahoo.co.jp:443



(adsbygoogle = window.adsbygoogle || []).push({});


(adsbygoogle = window.adsbygoogle || []).push({});

-1. システムエンジニアリング, ASP.NET Core, 実装技術

執筆者:

関連記事

Javaでサポートする暗号化アルゴリズム

Java暗号化アーキテクチャ Javaで暗号化処理を実装する場合、Java暗号化アーキテクチャ(Java Cryptography Architecture: JCA)と呼ばれるフレームワークを使いま …

Wildfly11のインストール

このサイトでWebアプリを公開したい。 仕事ではJBoss Enterprise Application Server(JBoss EAP)を使う機会が多いのでそれを使いたいがライセンス料がかかる。無 …

Java/JavaEE開発キットの作成

Javaアプリ開発者のローカルPCに、ファイル展開するだけで開発環境をできるようにするための開発キットの準備について記載します。 ★随時更新予定★ 背景 Java/JavaEE等を使ったアプリ開発では …

開発環境のJBoss EAP7にリモートアクセス

開発中のものを他者に見せたり、問題が発生している開発者の開発物を参照するために、eclipse上で起動しているEAP7のWebアプリに別のPCからアクセスしたい場合があります。 このための手順を記載し …

CentOS7のマルチホーム化

サイトの存在を隠しつつも、sftpサーバを公開し、後輩と1G以上のファイルのやりとりしたい。 パブリック側のIPアドレスを教えてしまうと、どこのサーバだろうかとブラウザで開いたりするとサイトの存在がわ …

プロフィール ゆっきーです。
都内でシステムエンジニアをやっています。
もっと詳細を見る