ASP.NET Core Razor Pages アプリでファイルをアップロードする方法について書きます。ターゲットプラットフォームはこの記事を書いた時点での最新 LTS 版の .NET 8.0 です。
普通に form を submit して POST 送信する場合と、fetch API を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は fetch API を使ってアップロードした結果です。
この記事を読む前に、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。
この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。
セキュリティの話はちょっと置いといて、この記事では単純にファイルをアップロードするにはどうするかということを書きます。気をつけるべき点は以下の通りです。
-
cshtml のコードでは form 要素の enctype 属性に "multipart/form-data" を設定する。
-
cshtml.cs の OnPost メソッドが受け取るモデルの、アップロードされたファイルがバインドされるプロパティは IFormFile 型とする。
-
上に述べたプロパティの名前は、html ソースの
<input type="file" ... />
の name 属性と一致させる。 -
ブラウザによってはクライアント PC でのフルパスがファイル名として送信されることがあるので、Path.GetFileName を使ってパスを除いたファイル名のみを取得する。
-
ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。(IIS でホストする場合です)
-
IIS も Kestrel も最大要求本文サイズに 30,000,000 バイトの制限がある。詳しくは上に紹介した Microsoft の記事の「IIS」または「Kestrel の最大要求本文サイズ」のセクションを見てください。変更方法も書いてあります。
fetch API を使ってファイルをアップロードする場合は、上記に加えて以下の点に注意してください。
-
fetch API を使用して送信するフォームデータを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。FormData オブジェクトには CSRF 防止のための隠しフィールドのトークンも含まれます。同時にクッキーの CSRF 防止のためのトークンも送信されるので、サーバー側で検証が可能になります。
-
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();
}
}
}
}