WebSurfer's Home

APMLフィルター

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

by WebSurfer 2025年1月2日 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 に、

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

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

1
2
3
4
5
6
// 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 の設定は不要で、以下のようにすれば良いです。

1
2
3
// 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 を設定しないと対話型にならないので注意してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@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 というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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 行を追加します。

1
2
3
4
5
6
7
// 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 でのキャプチャ画像

最初のレートをつける

  • Currently .0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , , ,

Upload Download

HttpClient の同時接続数

by WebSurfer 2024年12月21日 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 のテンプレートで作ったものです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
namespace ConsoleAppLocalhost
{
    internal class Program
    {
        static readonly HttpClient client = new();
 
        static async Task Main(string[] args)
        {
            // localhost
            // ローカルの IIS Express で動くASP.NET MVC5 アプリの
            // アクションメソッド。
            // 要求を受けて 10 秒後に OK という文字列を返す
            //var uri = "https://localhost:44365/Home/sample";
 
            // ホスティングサービスの IIS で動く ASP.NET Web Forms
            // アプリの HTTP ジェネリックハンドラ。
            // 要求を受けて 10 秒後に Hello World という文字列を返す
            var uri = "https://example.com/xxxxxxxx";
 
            // websiteproject.com
            // ローカル IIS で動く ASP.NET Web Forms アプリの HTTP
            // ジェネリックハンドラ。
            // hosts ファイルで 127.0.0.1 に websiteproject.com と
            // いうホスト名を付けたのでそのホスト名で呼び出せる。
            // 要求を受けて 10 秒後に Hello World という文字列を返す
            //var uri = "http://websiteproject.com/Sample.ashx";
 
            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();
        }
    }
}

最初のレートをつける

  • Currently .0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: ,

CORE

Visual Studio 2022 で Python Flask

by WebSurfer 2024年12月15日 14:55

先の記事「Flask 開発サーバーが .mjs に Contents-Type: text/plain を返す」と話が前後しますが、その記事を書くため自分の Windows 10 PC の Visual Studio 2022 17.12.3 で Python Flask アプリが動かせるよう設定しました。

Visual Studio 2022 で Python Flask

いろいろ紆余曲折があってスムーズにはいかなかったので、今後同じことを行うことがあったらその際にまた苦労しなくて済むよう、その紆余曲折を備忘録として残しておくことにします。

まず、Microsoft のドキュメント「チュートリアル: Visual Studio での Flask Web フレームワークの概要」の「前提条件」のセクションに従って Visual Studio Installer を起動し以下のワークロードと個別のコンポーネントを追加します。

  • 「Python 開発」ワークロード
  • 「個別のコンポーネント」タブの「コード ツール」下の [Git for Windows]

その後で「手順 1-1: Visual Studio プロジェクトとソリューションを作成する」に進みましたが、その手順に書いてある [Blank Flask Web Project] テンプレートが出てきません。(汗)

別の Microsoft のドキュメント「クイック スタート: Visual Studio で Python Web アプリを作成」を見ると、

"Visual Studio インストーラーで [Python 開発] ワークロードを選択し、インストールの詳細で [Python Web サポート] を選択します。"

・・・と書いてあるのでその通りにすると Python の Web アプリ関係のテンプレートはいろいろ出てくるようになりました。

上に紹介した 1 つ目の Microsoft ドキュメントには Git を使う手順がいろいろ書いてあるのですが、Flask とは直接関係ない Git の勉強に余計な労力を使いそうだったので、これ以降は 2 つ目のドキュメントに従って進めることにします。

プロジェクトの新規作成メニューで表示されたテンプレートの中で [Web プロジェクト] を選んでプロジェクトを作成し先に進めます。

「Web プロジェクト」テンプレート

Flask ライブラリをインストールする」のセクションまで進んで、

"1. プロジェクトの [Python 環境] ノードを展開して、プロジェクトの既定の環境を表示します。"

・・・をやってみましたが [Python 環境] 下には何も表示されません。(汗) 以下の操作を行う必要がありました。

ソリューションエクスプローラーで [Python 環境] ノードを右クリックして表示されたメニューから [環境を追加(E)...] をクリック、「環境を追加」ダイアログの左側のメニューの [Python インストール] を選択し、[Python 3 64 ビット (3.9.13)] を追加します。(サポート外) と表示されていましたが、後でアップデートすることにして、そのままインストールしました。

その操作により、ソリューションエクスプローラーの [Python 環境] 下に「Python 3.9 (64-bit) (グローバルデフォルト)」が表示されます。

「Python 3.9 (64-bit) (グローバルデフォルト)」を右クリックして [Python パッケージの管理] を選択します。 Microsoft のドキュメントに書いてある通り [パッケージ (PyPI)] タブ上に [Python 環境] ウィンドウが開きます。

検索フィールドに flask と入力し「次のコマンドを実行する: pip install flask」をクリックします。Microsoft のドキュメントに書いてある通り、ライブラリがソリューションエクスプローラーの [Python 環境] 下の「Python 3.9 (64-bit) (グローバルデフォルト)」下に表示されます。

コード ファイルの追加」のセクションに進んで最小限のウェブアプリケーションを実装します。

ソリューションエクスプローラーでプロジェクトのノードを右クリックし、表示されたメニューから [追加 (D)] ⇒ [新しい項目(W)...] と進んで、表示される「新しい項目の追加」ダイアログで [空の Python ファイル] を選び、名前を app.py として [追加] ボタンをクリックします。

Microsoft のドキュメントに書いてあるコード(下記)をコピーしてエディタウィンドウに貼り付けて保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask
 
# Create an instance of the Flask class that is the WSGI application.
# The first argument is the name of the application module or package,
# typically __name__ when using a single module.
app = Flask(__name__)
 
# Flask route decorators map / and /hello to the hello function.
# To add other resources, create functions that generate the page contents
# and add decorators to define the appropriate resource locators for them.
 
@app.route('/')
@app.route('/hello')
def hello():
   # Render the page
   return "Hello Python!"
 
if __name__ == '__main__':
   # Run the app server on localhost:4449
   app.run('localhost', 4449)

アプリケーションの実行」のセクションに進みます。app.py を [スタートアップファイルとして設定] し、プロジェクトのプロパティ画面でポート番号を app.py に指定した 4449 に設定します。

Visual Studio のメニューバーの [デバッグ(D)] ⇒ [デバッグなしで開始(H)](もしくは [デバッグの開始(S)])で Flask 開発サーバーが立ち上がり、

Flask 開発サーバー

この記事の一番上の画像のようにブラウザが立ち上がって app.py に設定した文字列 Hello Python! が表示されます。

最初のレートをつける

  • Currently .0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , ,

その他

About this blog

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

Calendar

<<  2025年1月  >>
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar