WebSurfer's Home

Filter by APML

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

Blazor WASM で jQuery ajax を使ってファイルアップロード

by WebSurfer 28. October 2022 14:12

Blazor Web Assembly (WASM) アプリで、html の form 要素及び input type="file" 要素と jQuery ajax を使ってファイルをアップロードする方法を書きます。

Blazor WASM でファイルアップロード

(.NET 5.0 以降であれば、InputFile クラスを利用できるそうですので、そちらを使う方がいいかもしれません。詳細は Microsoft のドキュメント「ASP.NET Core Blazor ファイルのアップロード」を見てください)

jQuery / JavaScript を使ってファイルのアップロードはできますので、基本ブラウザである Blazor WASM のクライアント側からでも同様にファイルのアップロードはできます。

その jQuery / JavaScript のコードを Blazor WASM のクライアント側に組み込んで呼び出すことで、Blazor WASM アプリとしてファイルのアップロード機能を持たせることが可能です。(以下に概略を書きますが、詳細は Microsoft のドキュメント「ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す」を見てください)

なお、input type="submit" ボタンを form 要素内に配置して、それをクリックするというのは NG です。ファイルはアップロードされますが、返ってきた応答でブラウザの画面が書き換えられてしまうので。

jQuery ajax を使ってのファイルのアップロード機能をどのように実装するかの具体例を以下に書いておきます。

ベースとする Blazor WASM ソリューションは、Visual Studio 2022 のテンプレートを使って、フレームワーク .NET 6.0 で、[ASP.NET Core でホストされた(h)]にチェックを入れて作成したものを使います。ソリューション内に Client, Server, Shared という 3 つのプロジェクトができるはずです。

(1) ファイルアップロード用の Razor ページ

Client プロジェクトの Pages フォルダに以下の Razor ページを追加します。

@page "/fileupload"
@inject IJSRuntime JSRuntime;

<h3>File Upload by jQuery Ajax</h3>

<form id="form1" method="post" enctype="multipart/form-data">
    <input type="file" name="postedfile" />
</form>

<button type="button" class="btn btn-primary" @onclick="UploadFile">
    アップロード
</button>

<div id="result"></div>

@code {
    private async Task UploadFile()
    {
        await JSRuntime.InvokeVoidAsync("uploadFile");
    }
}

Shared フォルダの NavMenu.razor に上の fileupload ページへのリンクを追加し、メニューからリンクをクリックしてアクセスできるようにします。

(2) ファイルアップロード用の js ファイル

ファイルをアップロードする jQuery / JavaScript コードのコードを含んだ js ファイルを作成します。コードは以下の通りです。js ファイルの名前は任意ですがここでは exampleJsInterop.js としておきます。

window.uploadFile = () => {
    // FormData オブジェクトの利用
    var fd = new FormData(document.getElementById("form1"));

    $.ajax({
        url: '/Upload',
        method: 'post',
        data: fd,
        processData: false, // jQuery にデータを処理させない
        contentType: false  // contentType を設定させない
    }).done(function (response) {
        $("#result").empty;
        $("#result").text(response);
    }).fail(function (jqXHR, textStatus, errorThrown) {
        $("#result").empty;
        $("#result").text('textStatus: ' + textStatus +
            ', errorThrown: ' + errorThrown);
    });
}

(3) js ファイルの配置

上の exampleJsInterop.js と jQuery.js を、Client プロジェクトの wwwroot フォルダ下に js フォルダを作って、その中に配置します。

js ファイルの配置

(4) index.html に script タグを追加

Client プロジェクトの wwwroot フォルダ下の index.html に、exampleJsInterop.js と jQuery.js がダウンロードされるよう、script タグを追加します。

Index.html に script タグを追加

(5) Server プロジェクトに Controller 追加

Server プロジェクトの Controllers フォルダにアップロードされたファイルを受け取って必要な処置をする Controller を追加します。

具体例は以下のコードを見てください。Web API の Controller としています。名前をステップ (2) の jQuery / JavaScript のコードにある url: '/Upload' と合わせる必要があることに注意してください。

using Microsoft.AspNetCore.Mvc;

namespace BlazorWasmCoreHosted.Server.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        // .NET 6.0 で作ったプロジェクトは「Null 許容」オプションが「有効化」に
        // 設定してあるので、引数も NULL 許容にしておかないと、クライアントでファ
        // イルを選択しないで送信した場合、HTTP 400 Bad Request エラーになって
        //  "The postedFile field is required."というエラーメッセージが返ってくる
        [HttpPost]
        public string Post([FromForm] IFormFile? postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得
                string filename = System.IO.Path.GetFileName(postedFile.FileName);

                // アップロードされたファイルを処理(省略)

                result = $"{filename} ({postedFile.ContentType}) - " +
                    $"{postedFile.Length} bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return result;
        }
    }
}

アプリを実行してファイルアップロードに成功するとこの記事の一番上にある画像のようになります。

Tags: , , ,

Upload Download

About this blog

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

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar