WebSurfer's Home

Filter by APML

Web API に Blob をアップロード

by WebSurfer 4. February 2025 12:28

ASP.NET Core Web API で、ブラウザから fetch や axios を使って multipart/form-data 形式でアップロードされてきた Blob データをどのように取得できるかということを書きます。

axios のドキュメント Multipart Bodies のサンプルコード(抜粋下記)を見て、そこに書いてある Blob データを Web API でどのように取得できるかを考えたのがきっかけです。

const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Blob([1,2,3]));
form.append('my_file', fileInput.files[0]);

上のコードの FormData を、ブラウザから JavaScript の fetch や axios を使って multipart/form-data 形式で送信すると、ボディ部分は下のようになります (Fiddler によるキャプチャ画像)。2 つ目の name が my_buffer となっているパートが Blob データです。

Fiddler によるキャプチャ画像

上のように送信されてきた my_buffer の Blob データは、ASP.NET Core Web API のアクションメソッドの引数の型を IFormFile 型とすれば取得できました。

具体的には、Web API プロジェクトで以下のクラスを定義し、

public class Blob
{
    public string? My_field { get; set; }
    public IFormFile? My_buffer { get; set; }
    public IFormFile? My_file { get; set; }
}

それをアクションメソッドの引数に設定して、JavaScript の fetch や axios を使って FormData をアクションメソッドに POST 送信すれば、下の画像の通り My_buffer プロパティに Blob を取得できます。

My_buffer プロパティに Blob を取得

MDN のドキュメント Blob に、

"File インターフェイスは Blob をベースにしており、 Blob の機能を継承してユーザーのシステム上のファイルをサポートするように拡張しています"

・・・と書いてあるとおり、input type="file" を使ってのファイルと同様の扱いになるということのようです。

最初、バイト配列として取得できるのではと思って、上の Blob クラスの My_buffer プロパティの型を byte[]? として試してみたのですが、バインドできないようで null になってしまいます。

My_buffer プロパティが byte[]? 型の場合

以下に検証に使ったコードを載せておきます。Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET 9.0 で作成した ASP.NET Core Web API アプリです。

Controller

using Microsoft.AspNetCore.Mvc;
using WebApi.Models;

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        [HttpPost("blob")]
        public async Task<IActionResult> ReceiveBlob([FromForm] Blob model)
        {
            string result = $"My_field: {model.My_field}";

            if (model.My_file != null && model.My_file.Length > 0)
            {
                string filename = Path.GetFileName(model.My_file.FileName);
                result += $", My_file: {filename}";
            }

            if (model.My_buffer != null && model.My_buffer.Length > 0)
            {
                string array = string.Empty;
                using (var stream = new MemoryStream())
                {
                    await model.My_buffer.CopyToAsync(stream);
                    byte[] bytes = stream.ToArray();
                    foreach (byte b in bytes)
                    {
                        array += $"[{b:x2}]";
                    }
                }
                result += $", My_buffer: {array}";
            }

            return Content(result);
        }
    }
}

View

@{
    ViewData["Title"] = "SendBlob";
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewData["Title"] - WebApi</title>
    <script type="text/javascript">
        const url = "/api/upload/blob";

        const upload = async () => {
            const fileInput = document.getElementById("fileupload");
            const resultDiv = document.getElementById("result");
            const form = new FormData();
            form.append('my_field', 'my value');
            form.append('my_buffer', new Blob([1, 2, 3]));
            form.append('my_file', fileInput.files[0]);

            const param = {
                method: "POST",
                body: form
            }
            const response = await fetch(url, param);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        };

        window.addEventListener('DOMContentLoaded', () => {
            const btn = document.getElementById("button1");
            btn.addEventListener("click", upload);
        });
    </script>
</head>
<body>
    <input type="file" name="fileupload" id="fileupload" multiple="multiple" />
    <br />
    <button type="button" id="button1">Upload</button>
    <br />
    <div id="result"></div>
</body>
</html>

上の View のコードの実行結果は以下のようになります。

View のコードの実行結果

(メモ: プロジェクトは VS2022 AspNet9 WebApi)

Tags: , , ,

Upload Download

ASP.NET Core Web API と Swagger(その1)

by WebSurfer 22. October 2024 18:00

Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET 8.0 で ASP.NET Core Web API のプロジェクトを作成し、Visual Studio から実行すると下の画像のようにブラウザ上に swagger/index.html というページが表示され、そこから Web API のアクションメソッドを要求して応答を調べることができます。(追記: 2024/11/25 時点で、ターゲットフレームワーク .NET 9.0 で作成した Web API プロジェクトには Swagger は含まれません)

Swagger

Swagger を使って (1) ファイルをアップロードする方法、及び (2) ベアラトークンを要求ヘッダに含めて送信する方法を調べましたので、備忘録として残しておくことにしました。

(1) と (2) を一つの記事に書くと長くなりすぎるので、この記事では (1) を書いて、(2) は別の記事に「その2」として書くことにします。

(1) ファイルアップロード

アクションメソッドの引数に IFormFile 型の変数を含めれば、Swagger が自動的にそれを検出してアップロードするファイルの選択が可能になり、Swagger から Web API にファイルをアップロードできるようになります。

Swagger にファイル選択のための画面を表示するには、まず、[Try it out]ボタンをクリックします。

[Try it out]ボタンをクリック

[Try it out]ボタンをクリックすると、下のように、ブラウザが html の <input type="file" > を表示した時と同様なファイル選択を行うための画面が表示されます。

アップロードするファイルの選択

その画面でアップロードするファイルを選択したら、[Execute]ボタンをクリックすれば選択したファイルは multipart/form-data 形式でサーバーに送信され、アクションメソッドの引数に渡されます。

下の Fiddler による要求のキャプチャ画像を見てください。

Fiddler による要求のキャプチャ画像

アクションメソッドへの引数に IFormFile 型の変数を含める方法ですが、下のコード例の SampleA メソッドのように直接含めても、SampleB メソッドのようにモデル経由で含めても、Swagger が自動的にそれを検出してくれます。上の画像は SampleA のものですが、SampleB でも同様になります。

using Microsoft.AspNetCore.Mvc;

namespace WebApi2.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        [HttpPost("SampleA")]
        public IActionResult SampleA(IFormFile? postedFile, 
                                     [FromForm] string? customField)
        {
            if (postedFile == null || postedFile.Length == 0)
            {
                return Content("ファイルを受信できませんでした");
            }

            if (customField == null)
            {
                return Content("customField を受信できませんでした");
            }

            return Content($"ファイル: {postedFile.FileName}, " +
                $"customField: {customField} 受信");
        }

        [HttpPost("SampleB")]
        public IActionResult SampleB([FromForm] UploadModels model)
        {
            if (model.PostedFile == null || model.PostedFile.Length == 0)
            {
                return Content("ファイルを受信できませんでした");
            }

            if (model.CustomField == null)
            {
                return Content("customField を受信できませんでした");
            }

            return Content($"ファイル: {model.PostedFile.FileName}, " +
                $"customField: {model.CustomField} 受信");
        }
    }

    public class UploadModels
    {
        public string? CustomField { get; set; }
        public IFormFile? PostedFile { get; set; }
    }
}

この記事の本題とは関係ない話ですが、上のサンプルコードで、SampleA の引数の型、SampleB が使う UploadModels のプロパティの型が null 許容型となっているのには理由がありますのでそれも書いておきます。

ユーザーがファイルを選択しないまま / customFiled が空白のまま[Execute]ボタンをクリックすると、null をアクションメソッドの引数にバインドしようとします。なので、引数を null 許容型にしておかないと、バインド時にエラーとなって HTTP 400 Bad Request が応答として返され、アクションメソッドは実行されません。

上のコード例で言うと、if 文以下は実行されないので期待した応答("customField を受信できませんでした" とか "ファイルを受信できませんでした")は返ってこないということになります。

Tags: , , , ,

DevelopmentTools

Razor Pages アプリでファイルアップロード

by WebSurfer 6. May 2024 13:10

ASP.NET Core Razor Pages アプリでファイルをアップロードする方法について書きます。先の記事「ASP.NET Core MVC でファイルアップロード」の Razor Pages 版です。MVC ⇒ Razor Pages にしたこと以外で先の記事と違うのは、ターゲットプラットフォームを .NET 8.0 にしたことと、jQuery ajax に代えて fetch API を使ったことです。

Razor Pages でファイルアップロード

普通に form を submit して POST 送信する場合と、fetch API を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は fetch API を使ってアップロードした結果です。

先の記事にも書きましたが、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。

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

セキュリティの話はちょっと置いといて、この記事では単純にファイルをアップロードするにはどうするかということを書きます。気をつけるべき点は以下の通りです。

  1. cshtml のコードでは form 要素の enctype 属性に "multipart/form-data" を設定する。
  2. cshtml.cs の OnPost メソッドが受け取るモデルの、アップロードされたファイルがバインドされるプロパティは IFormFile 型とする。  
  3. 上に述べたプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
  4. ブラウザによってはクライアント PC でのフルパスがファイル名として送信されることがあるので、Path.GetFileName を使ってパスを除いたファイル名のみを取得する。  
  5. ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。(IIS でホストする場合です。Kestrel の場合も権限が必要なのは同じだと思いますが詳しいことは未調査で分かりません)
  6. IIS も Kestrel も最大要求本文サイズに 30,000,000 バイトの制限がある。詳しくは上に紹介した Microsoft の記事の「IIS」または「Kestrel の最大要求本文サイズ」のセクションを見てください。変更方法も書いてあります。

fetch API を使ってファイルをアップロードする場合は、上記に加えて以下の点に注意してください。

  1. fetch API を使用して送信するフォームデータを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。FormData オブジェクトには CSRF 防止のための隠しフィールドのトークンも含まれます。同時にクッキーの CSRF 防止のためのトークンも送信されるので、サーバー側で検証が可能になります。
  2. fetch API を使う場合、デフォルトでは要求ヘッダに X-Requested-With: XMLHttpRequest は含まれない。サーバー側での判定のためなどにそのヘッダが必要な場合、クライアントスクリプトにそのヘッダを追加するコードを書く必要があります。

サンプルコードを以下に載せておきます。この記事の一番上の画像は下のサンプルコードの実行結果です。

Model

namespace RazorPages1.Models
{
    public class UploadModels
    {
        public string? CustomField { get; set; }
        public IFormFile? PostedFile { get; set; }
    }
}

cshtml

@page
@model RazorPages1.Pages.File.UploadModel

@{
    ViewData["Title"] = "Upload";
}

<style type="text/css">
    /*Bootstrap5 には form-group は無いので 4 と同じものを追加*/
    .form-group {
        margin-bottom: 1rem;
    }
</style>

<h1>Upload</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <div class="form-group">
                <div class="col-md-10">
                    <p>Upload file using this form:</p>
                    @* name 属性はモデルのプロパティ名と同じにする
                    こと。大文字小文字の区別はない*@
                    <input type="file" name="postedfile" />
                </div>
            </div>
            <div class="form-group">
                <div class="col-md-10">
                    <input type="submit" value="Submit Form"
                           class="btn btn-primary" />
                    <div>@Model.Message</div>
                </div>
            </div>
        </form>

        <div class="form-group">
            <div class="col-md-10">
                <input type="button" id="ajaxUpload"
                    value="Use Fetch API" class="btn btn-primary" />
                <div id="result"></div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        document.getElementById("ajaxUpload")
            .addEventListener('click', async () => {
                const fd = new FormData(document.querySelector("form"));
                const result = document.getElementById("result");

                // 追加データを以下のようにして送信できる。フォーム
                // データの一番最後に追加されて送信される
                fd.append("CustomField", "This is some extra data");

                const param = {
                    method: "POST",
                    body: fd,
                    // jQuery ajax と違って X-Requested-With ヘッダは
                    // 自動的には送られないので以下の設定で対応
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                }

                const response = await fetch("/file/upload", param);
                if (response.ok) {
                    const data = await response.text();
                    result.innerText = data;
                } else {
                    result.innerText = "response.ok が false";
                }
            });

        //]]>
    </script>
}

cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPages1.Models;

namespace RazorPages1.Pages.File
{
    public class UploadModel : PageModel
    {
        // Core では Server.MapPath が使えないことの対応
        private readonly IWebHostEnvironment _hostingEnvironment;

        public UploadModel(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }
        
        public void OnGet()
        {
        }

        public string? Message { get; set; }

        [BindProperty]
        public UploadModels Model { get; set; } = default!;

        public async Task<IActionResult> OnPostAsync()
        {
            string result;
            IFormFile? postedFile = Model.PostedFile;
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得。ブラウザによっ
                // ては postedFile.FileName はクライアント側でのフル
                // パスになることがあるので Path.GetFileName を使う
                string filename = Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string rootPath = _hostingEnvironment.ContentRootPath;

                // アプリケーションルートの UploadedFiles フォルダに
                // ファイルを保存する
                string filePath = $"{rootPath}\\UploadedFiles\\{filename}";
                using (var fs = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(fs);
                }

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

            // クライアントスクリプトによるアップロードか否かを判定。
            // fetch API を使う場合は X-Requested-With: XMLHttpRequest
            // ヘッダを送るようクライアントスクリプト側でコーディング
            // が必要
            if (Request.Headers.XRequestedWith == "XMLHttpRequest")
            {
                return Content(result);
            }
            else
            {
                Message = result;
                return Page();
            }
        }
    }
}

Tags: , , ,

Upload Download

About this blog

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

Calendar

<<  February 2025  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
242526272812
3456789

View posts in large calendar