WebSurfer's Home

Filter by APML

MVC5 で IHttpClientFactory を DI で利用

by WebSurfer 27. January 2025 14:26

下の図の構成で、ASP.NET MVC5 において Dependency Injection (DI) により IHttpClientFactory を取得し、それから HttpClient を生成して Web API からデータを取得し、そのデータをブラウザに表示する方法を書きます。

MVC5 で IHttpClientFactory を利用

ASP.NET Core MVC の例は先の記事「ASP.NET と HttpClient (CORE)」に書きました。

ASP.NET Core の場合は、Visual Studio のテンプレートを使ってプロジェクトを作成すればデフォルトで DI 機能が実装されるので、Program.cs で AddHttpClient メソッドを使って IHttpClientFactory を IServiceCollection (DI コンテナ) に登録するだけで、Controller はコンストラクタの引数経由で IHttpClientFactory を取得できます。

一方、ターゲットフレームワークが .NET Framework 場合、Visual Studio のテンプレートで作成した MVC5 アプリには DI 機能は実装されません。なので、最初の課題はそれにどのように DI 機能を追加するかになります。

ちなみに DI を利用して IHttpClientFactory を取得するのは必須です。何故かと言うと、Microsoft のドキュメント「IHttpClientFactory の代替手段」に書いてありますように、以下のことを回避できるというメリットがあるからです。

  • HttpMessageHandler インスタンスをプールすることによるリソース枯渇の問題
  • 一定の間隔で HttpMessageHandler インスタンスを循環させることによって発生する古くなった DNS の問題

ターゲットフレームワークが .NET Framework のアプリでも、バージョンが 4.6.2 以降であれば ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を利用して DI 機能を実装できます。

.NET Framework 4.8 の MVC5 アプリに DI 機能を実装する詳しい方法は、先の記事「MVC5 での Dependency Injection」に書きました。この記事では、その記事のアプリの IServiceCollection (DI コンテナ) に IHttpClientFactory を登録し、それを Controller でどのように使って Web API に要求を出し、データを取得するかを書きます。

(1) Microsoft.Extensions.Http

NuGet パッケージ Microsoft.Extensions.Http をインストールします。これは AddHttpClient メソッドを使用して IHttpClientFactory を IServiceCollection(DI コンテナ)に登録できるようにするため必要です。

Microsoft.Extensions.Http

(2) AddHttpClient メソッドの追加

先の記事「MVC5 での Dependency Injection」に書いた Global.asax.cs のコード内の ConfigureServices メソッドに、以下のように AddHttpClient メソッドを追加します。この一行で Controller はコンストラクタの引数経由で IHttpClientFactory を受け取れるようになります。

// DI コンテナにサービスを登録するメソッド
private void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ScopedThing>();
    services.AddTransient<HomeController>();

    // IHttpClientFactory を DI して利用できるよう追加
    services.AddHttpClient();
}

(3) Controller

Controller の例です。ブラウザが Hero アクションメソッドを呼び出すと、DI 機能により IHttpClientFactory が Controller のコンストラクタの引数経由で渡されます。

IHttpClientFactory から生成した HttpClient を使って Web API に要求を出して JSON 形式のデータを取得し、それを .NET の List<Hero> 型のオブジェクトにデシリアライズして View に渡しています。

using Mvc5DependencyInjection.Models;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Text.Json;

namespace Mvc5DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private readonly ScopedThing _scopedThing;

        // IHttpClientFactory を DI して利用できるよう追加
        private readonly IHttpClientFactory _clientFactory;

        // コンストラクタの引数経由 IHttpClientFactory が DI される
        public HomeController(ScopedThing scopedThing, 
                              IHttpClientFactory clientFactory)
        {
            this._scopedThing = scopedThing;
            this._clientFactory = clientFactory;
        }

        // ・・・中略・・・

        public async Task<ActionResult> Hero()
        {
            // IHttpClientFactory から HttpClient を取得
            HttpClient client = _clientFactory.CreateClient();

            var url = "Web API の url";
            var request = new HttpRequestMessage(HttpMethod.Get, url);
            HttpResponseMessage response = await client.SendAsync(request);
            List<Hero> list = null;

            if (response.IsSuccessStatusCode)
            {
                using (Stream responseStream =
                              await response.Content.ReadAsStreamAsync())
                {
                    list = await JsonSerializer.
                           DeserializeAsync<List<Hero>>(responseStream);
                }
            }

            return View(list);
        }
    }
}

(4) Model

namespace Mvc5DependencyInjection.Models
{
    public class Hero
    {
        public int id { get; set; }
        public string name { get; set; }
    }
}

(5) View

@model IEnumerable<Mvc5DependencyInjection.Models.Hero>

@{
    ViewBag.Title = "Hero";
}

<h2>Hero</h2>

<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.id)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.name)
        </th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.id)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.name)
            </td>
        </tr>
    }

</table>

(6) 実行結果

上のコードを実行するとブラウザには以下のように表示されます。

実行結果

Tags: , , ,

MVC

Blazor Web App で HttpClient を使ってファイルアップロード

by WebSurfer 2. January 2025 14:42

Blazor Web App から HttpCLient を使ってファイルアップロードする方法を書きます。ベースとしたのは Visual Studio 2022 のテンプレートを使って、ターゲットフレームワークを .NET 9.0 に、 [Interactive render mode] を [WebAssembly] に、 [Interactive location] を [Per page/component] に設定して作成したプロジェクトです。設定が異なる場合は下の説明の中には当てはまらない事がありますので注意してください。

Blazor Web App で HttpClient を使ってファイルアップロード

JavaScript の fetch を使ってアップロードする例は先の記事「Blazor Web App でファイルアップロード」に書きました。それに比べて、HttpClient を使���場合は、Program.cs での HttpClient の登録が必要なこと、本来ブラウザでは使えない HttpClient を使うことの違和感などが気になりました。それでも Blazor では JavaScript より HttpClient を使うのが本筋のようです。

この記事の例では、ファイルのアップロード先は外部 Web API または本 Blazor アプリのプロジェクト内に実装した Web API としました。その Web API のアクションメソッドに Blazor Web App の Razor コンポーネントから HttpClient を使ってファイルを送信します。(実際にはブラウザから送信されますので、HttpClient から生成された WebAssembly が送信しているのだろうと思いますが、具体的にどうなっているのかは分かりません)

Visual Studio でプロジェクトを作成すると、下のソリューションエクスプローラーの画像の構成となります。青枠と赤枠の中のフォルダ / ファイルはこの記事を書く際に追加したものです。青枠がプロジェクト内に作成した Web API とファイル保存用のフォルダ、赤枠がファイルをアップロードする Razor コンポーネントです。

プロジェクトの構成

この記事の例は、Microsoft のドキュメント「クライアント側のレンダリング (CSR) を使用するサーバーへのファイルのアップロード」に該当します。それを読んですべて分かれば良いのですが、自分は読んでも分からないことがありました。

なので、この記事に、Microsoft のドキュメントを読んでも分からなかったことを調べて書くとともに、実装を基本的な部分のみに簡略化したコードを備忘録として載せることにしました。

(1) InputFile クラスの使用

html の <input type="file"> を使った場合は、ユーザーが選択したファイルのデータを HttpClient の C# のコードに渡すことができません (裏ワザとかがあるかもしれませんが)。

InputFile クラスを使用すると、ユーザーがファイルの選択を完了したときに発生する OnChange イベントのハンドラの引数に InputFileChangeEventArgs オブジェクト が渡され、ユーザーが選択したファイルのデータを HttpClient の C# のコードで取得できます。

それゆえ、アップロードするのに HttpClient を使う場合は、InputFile クラスを使用するほか選択肢はなさそうです。

(2) Razor コンポーネントの配置場所

上のソリューションエクスプローラーの画像に示すように、メインプロジェクトとクライアントプロジェクト (.Client) の 2 つのプロジェクトが作られます。そのどちらにも Razor コンポーネントを配置できるのですが、今回のファイルをアップロードする Razor コンポーネントはどちらに配置すればいいのでしょうか?

答えはクライアントプロジェクト (.Client) 側です。上のソリューションエクスプローラーの画像の赤枠で示した FileUpload.razor がそれです。理由は以下の通りです。

Microsoft のドキュメント「ASP.NET Core Blazor プロジェクトの構造」に "対話型 WebAssembly または対話型自動レンダリング モードを使用するコンポーネントは、.Client プロジェクトに配置する必要があります" と書いてあります。

「対話型」の対話というのはユーザーとの対話を意味します。例えばテンプレートで自動生成されるコンポーネント Counter.razor のようにユーザーのクリックに応じてカウント値が変わっていくものが該当するようです。InputFile クラスの OnChange イベントで処理を行うのも対話に含まれるようです。

実は、最初 FileUpload.razor はメインプロジェクトに配置していたのですが、OnChange のハンドラが動かず、その解決に半日ほどハマったのは内緒です。(笑)

(3) Program.cs で HttpClient サービスの追加

HttpClient サービスの追加をメインプロジェクとクライアントプロジェクト (.Client) 両方の Program.cs で行う必要があります。

上に紹介した Microsoft のドキュメントに書いてある通り、メインプロジェクトの Program.cs に、

// IHttpClientFactory および関連するサービスを追加
builder.Services.AddHttpClient();

クライアントプロジェクト (.Client) の Program.cs に、

// Web API に対する POST 要求のための HttpClient の登録
builder.Services.AddScoped(sp =>
    new HttpClient 
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

・・・を追加します。(注: 後者のコードは「Blazor WebAssembly スタンドアロン アプリ」のテンプレートを使って作成した Blazor WebAssembly プロジェクトの Prpgram.cs には最初から含まれています)

でも、考えてみると HttpClient を使うのはクライアントプロジェクト (.Client) の FileUpload.razor なのに、なぜメインプロジェクトの Program.cs でも追加しなければならないのかが腑に落ちません。

Microsoft のドキュメントには "クライアント側コンポーネントはサーバーでプリレンダリングされるため、HttpClient サービスをメインプロジェクトに追加する必要があります。対話型コンポーネントではプリレンダリングが既定で有効になっています" と書いてありました。そう言われても、その仕組みは全く理解できていませんが。(汗)

上のクライアントプロジェクト (.Client) の Program.cs での設定例では、HttpClient の BaseAddres プロパティを設定していますがその理由を書きます。

builder.HostEnvironment.BaseAddress というのは Blazor アプリのベースアドレスで、この記事の例では https://localhost:44340/ となります。

Microsoft のドキュメント「HttpClient.BaseAddress プロパティ」に "相対 URI を使用して HttpRequestMessage を送信すると、メッセージ Uri が BaseAddress プロパティに追加され、絶対 URI が作成されます" と書かれています。

なので、Blazor アプリ内の Web API (上のソリューションエクスプローラーの画像の青枠がそれ) にファイルを送信する場合は、上のコードのように BaseAddress プロパティを設定しておくと、FileUpload.razor の HttpClient のコードで URI を指定する際、相対 URI を使うことができるようになります。

外部 Web API にファイルを送信する場合は、FileUpload.razor の HttpClient のコードで絶対 URI を指定しますが、その場合はそれが HttpClient の BaseAddress に追加されるということはなく、コードで指定した絶対 URI がそのまま使用されます (ドキュメントにはそのことは書いてないですが検証して確認)。

HttpClient のコードで常に絶対 URI を指定するのであれば上のような HttpClient の BaseAddress の設定は不要で、以下のようにすれば良いです。

// FileUpload.razor の Http.PostAsync(url, content) の url に
// http から始まる絶対 URI を指定するのであれば以下で良い
builder.Services.AddScoped(sp => new HttpClient());

(4) FileUpload.razor

ユーザーがファイルを選択してアップロードする razor コンポーネント (FileUpload.razor) のサンプルコードは以下の通りです。Blazor Web App はデフォルトでは static rendering になるそうで、@rendermode InteractiveWebAssembly を設定しないと対話型にならないので注意してください。

@page "/fileupload"
@rendermode InteractiveWebAssembly
@using System.Net.Http.Headers
@inject HttpClient Http

<h3>Upload File</h3>

<InputFile OnChange="OnInputFileChange" multiple />
<br />
<p>@result</p>

@code {
    // 外部 Web API の URI
    // private string url = "https://localhost:44366/FileUpDownload/multiple";

    // Blazor アプリのプロジェクト内に作成した Web API の URI
    // Program.cs での HttpClient の登録で BaseAddress に本 Blazor 
    // アプリのベースアドレスを設定してあるので相対 URI を使用可
    private string url = "/FileSave";

    private string result = string.Empty;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        using var content = new MultipartFormDataContent();

        // GetMultipleFiles メソッドの引数 maximumFileCount は
        // デフォルトで 10。超えると例外がスローされる
        foreach (var file in e.GetMultipleFiles())
        {
            // OpenReadStream メソッドの引数 maxAllowedSize は
            // デフォルトで 500KB。超えると例外がスローされる
            var streamContent = new StreamContent(file.OpenReadStream());
            streamContent.Headers.ContentDisposition =
                new ContentDispositionHeaderValue("form-data")
                    {
                        // Name は Web API の引数名に合わせる
                        Name = "postedfiles",  
                        FileName = Path.GetFileName(file.Name)
                    };
            streamContent.Headers.ContentType =
                new MediaTypeHeaderValue(file.ContentType);

            content.Add(streamContent);
        }

        HttpResponseMessage response = await Http.PostAsync(url, content);

        // Web API からの応答のコンテンツの文字列を取得
        result = await response.Content.ReadAsStringAsync();
    }
}

上のコードでは、ユーザーがファイルの選択を完了すると、ファイルは即 Web API に送信されます。

それを、ユーザーがファイルを選択した後でボタンクリックによりアップロードしたい場合は、OnInputFileChange メソッドでは e.GetMultipleFiles() メソッドで IReadOnlyList<IBrowserFile> オブジェクトを取得してそれを保持するに留め、ボタンクリックのハンドラで保持された IReadOnlyList<IBrowserFile> オブジェクトからファイルデータを取得して送信するのが良さそうです。

(5) Web API

FileUpload.razor から送信されたファイルを受け取って UploadedFiles フォルダに保存する Web API のサンプルコードを下に載せておきます。

この記事のコードでは Microsoft の記事「ASP.NET Core でファイルをアップロードする」に書かれたセキュリティに関する配慮はされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、アプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するようになっています。

using Microsoft.AspNetCore.Mvc;

namespace BlazorWebAppWASM.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class FileSaveController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileSaveController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        public async Task<IActionResult> MuilipleFiles(List<IFormFile>? postedFiles)
        {
            string result = "Uploaded files: ";
            if (postedFiles != null)
            {
                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                foreach (var postedFile in postedFiles)
                {
                    if (postedFile != null && postedFile.Length > 0)
                    {
                        // アップロードされたファイル名を取得
                        string filename = Path.GetFileName(postedFile.FileName);

                        // アプリケーションルート直下の UploadedFiles フォルダに書き込み
                        string filePath = $"{contentRootPath}\\UploadedFiles\\{filename}";
                        using (var stream = new FileStream(filePath, FileMode.Create))
                        {
                            await postedFile.CopyToAsync(stream);
                        }

                        result += $"{filename} ";
                    }
                }
            }
            else
            {
                result = "postedFiles is null";
            }

            return Content(result);
        }
    }
}

Blazor アプリのプロジェクト内に実装する場合は、上のソリューションエクスプローラーの画像の青枠に示したようにメインプロジェクトに配置します。さらに、メインプロジェクトの Program.cs に下の 2 行を追加します。

// Controller を使用できるようサービスを追加
builder.Services.AddControllers();

// ・・・中略・・・

// 属性でルーティングされたコントローラーをマップ
app.MapControllers();

外部 Web API に上と同様なコードを実装して FileUpload.razor から送信されたファイルを受け取ることもできます。ただし、その場合はクロスドメインになるので、Web API 側のサーバーが CORS 対応している必要があります。

HttpClient を使っていると言っても、結局それは WebAssembly に変換されてブラウザに送信され、ブラウザから Web API に要求が出るので、クロスドメインでの要求は、JavaScript の fetch を使った場合と同様に、CORS 対応がされてないと失敗します。

外部 Web API のサーバーで CORS 対応がされていれば、下の Fiddler でのキャプチャ画像のように #95 でプリフライトリクエストが出て、#96 でファイルが送信されます。

Fiddler でのキャプチャ画像

Tags: , , , ,

Upload Download

HttpClient の同時接続数

by WebSurfer 21. December 2024 14:02

HTTP 1.1 仕様では同時接続数は 2 つまでとなっているそうで (過去の話らしい)、それに準じて .NET Framework 4.8 の HttpWebRequest を使ったアプリでも同時接続数は 2 つまでの制限がかかります。(要求先が localhost では無制限となります。詳しい話は先の記事「HttpWebRequest の localhost への同時接続数」を見てください)

その同時接続数の制限が、この記事を書いた時点での最新ターゲットフレームワーク .NET 9.0 の HttpClient を使ったアプリではどうなるかを調べたというのがこの記事の話です。

実は、後になって気が付いたのですが、Microsoft のドキュメント「System.Net.Http.HttpClient クラス」の「接続のプール」のセクションの「注意」に、

"同時接続の数を制限するには MaxConnectionsPerServer プロパティを設定します。既定では同時 HTTP/1.1 接続の数は無制限です"

・・・と書いてあって、わざわざコードを書いたりして調べるまでもなかったです。でも、せっかく調べたので、調べたことを以下に書いておきます。

surferonwww.info の場合

上の画像は、自分が使っているホスティングサービスの ASP.NET Web Forms アプリに、要求を受けて 10 秒後に Hello World という文字列を返す HTTP ジェネリックハンドラを作り、それに対して HttpClient を使ったマルチスレッドアプリから同時に 8 つの要求を出し、その応答を表示したものです。(同時要求 8 は検証に使った PC のコア数です。その数まではスレッドプールから一気にスレッドを取得できるそうです)

異なるスレッドで 8 つの要求が同時にサーバーに送信され、10 秒後に 8 つの応答が同時にクライアントに返されています。すなわち同時接続数の制限はされてないという結果でした。

ちなみに、HttpWebRequest のように同時接続数が 2 つまでという制限がかかると、2 を超えた分は前回の応答が返ってきてからでないとクライアントから要求が出ず、2 要求毎にサーバーでの処理時間の 10 秒ずつ待たされ、8 つ要求出した場合は全部終わるのに 40 秒かかります。

以下に検証に使った .NET 9.0 の HttpClient のコンソールアプリのコードを載せておきます。Visual Studio 2022 のテンプレートで作ったものです。

namespace ConsoleAppLocalhost
{
    internal class Program
    {
        static readonly HttpClient client = new();

        static async Task Main(string[] args)
        {
            // ホスティングサービスの IIS で動く ASP.NET Web Forms
            // アプリの HTTP ジェネリックハンドラ。
            // 要求を受けて 10 秒後に Hello World という文字列を返す
            var uri = "https://example.com/xxxxxxxx";

            var tasks = new List<Task>();

            // 同じ URL を 5 回同時に要求する
            foreach (var i in Enumerable.Range(0, 5))
            {
                var task = Task.Run(async () =>
                {
                    // ThreadId と開始時刻
                    int id = Thread.CurrentThread.ManagedThreadId;
                    string start = $" / ThreadID = {id}, " +
                                   $"start: {DateTime.Now:ss.fff}, ";

                    using HttpResponseMessage response = await client.GetAsync(uri);
                    response.EnsureSuccessStatusCode();
                    string responseBody = await response.Content.ReadAsStringAsync();
                    

                    // 終了時刻
                    string end = $"end: {DateTime.Now:ss.fff}";
                    responseBody += start + end;

                    Console.WriteLine(responseBody);
                });

                tasks.Add(task);
            }

            await Task.WhenAll(tasks);

            Console.WriteLine("Finish");
            Console.ReadLine();
        }
    }
}

Tags: ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  March 2025  >>
MoTuWeThFrSaSu
242526272812
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar