Blazor でリッチテキストエディターから画像をアップロードする

図:Home.razor が JS 相互運用でエディターを作成し、エディターはギャラリーとアップロードのリクエストを、SignalR の回線ではなく通常の HTTP で Program.cs のエンドポイントに送る

Blazor Server では、すべてをコンポーネント経由で送りたくなります。回線はすでに接続されており、C# もすぐそこにあるからです。しかし、エディターの画像アップロードではそれはうまくいかず、その理由は Blazor に特有のものです。このページでは、画像を同じアプリ内の通常の HTTP エンドポイントに送る理由、それらのエンドポイントを MapRazorComponents と並べて置く方法、そして Blazor のテンプレートの偽造防止ミドルウェアがそれらにどう作用するかを説明します。

回線を使わない理由

  • エディターは URL に POST します。ドラッグ&ドロップ、貼り付け、「Insert Image」ダイアログ、ギャラリーの「Upload」ボタンは、いずれも rte-files.js を通じて /rte/upload に multipart/form-data の XHR を送信します。Razor コンポーネントには、POST を受け付ける URL がありません。
  • 回線にはメッセージあたり 32 KB の制限があります。SignalR 経由でブラウザーからサーバーに送るメッセージは既定で 32 KB に制限されており、JS 相互運用の結果もこれに含まれます。InvokeAsync<string> でエディターの HTML を文字列として返したところ、20,000 文字は届きましたが、40,000 文字では「Connection closed with an error」で回線が閉じられました。画像のバイト列を相互運用で送ろうとしても、同じ壁にぶつかります。Blazor 自体の <InputFile> はストリーミングでこれを回避しますが、エディターは InputFile を使いません。

そのため、コンポーネントが行うのはエディターの作成だけで(方法は Blazor でリッチテキストエディターに画像ギャラリーを追加するで紹介しています)、ファイルはほかの ASP.NET Core アプリと同じように HTTP でやり取りします。

Program.cs

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();   // /uploads, /richtexteditor, /rte-files.js
app.UseAntiforgery();      // Blazor's own forms; the /rte endpoints opt out (RteEndpoints.cs)

app.MapRteFiles();         // GET+POST /rte/gallery, POST /rte/upload
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

MapRteFiles() は、3 つの最小 API のエンドポイントをマップします。GET と POST /rte/gallery、そして POST /rte/upload です。UseRteStaticFiles() は、保存された画像を /uploads で、エディターを /richtexteditor で、そして rte-files.js を配信します。これらはすべて回線と同じプロセスで動作するので、エンドポイントは同じサービスと同じサインインを利用できます。

テンプレートの偽造防止

Blazor Web App のテンプレートは、Blazor 自体のフォームのために app.UseAntiforgery() を呼び出します。.NET 8 では、IFormFile をバインドするエンドポイントには偽造防止のメタデータが付くため、このミドルウェアがあると、トークンのないアップロードは 400 になりました。Development 環境では、理由が「Invalid anti-forgery token found when reading parameter "IFormFile file" from the request body as form.」と表示されます。エディターの XHR にはトークンが含まれないため、2 つの POST エンドポイントでは偽造防止を無効にしています。

C# · RteEndpoints.cs
// Binding [FromForm] or IFormFile makes .NET 8 demand an antiforgery token on these endpoints.
// The editor's XHRs cannot send one, so it is switched off here and RteFiles checks the
// request's origin instead (see RequireSameOrigin). guide-notes.md has the details.
rte.MapPost("/gallery", (HttpContext http, [FromForm] string? action, [FromForm] string? folder, [FromForm] string? name) =>
    Json(RteFiles.CreateFolder(http, action, folder, name)))
    .DisableAntiforgery();
C# · RteEndpoints.cs
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
    Json(await RteFiles.Save(http, file, folder)))
    .DisableAntiforgery()
    // Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
    .WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
    .WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);

トークンの代わりに、RteFiles は Sec-Fetch-Site と Origin を使い、ブラウザーがほかのサイトから来たものとして示した POST を拒否します。Blazor 自体のフォームは保護されたままです。アップロード用のルートでは、リクエストの制限も、Kestrel の既定値 30,000,000 バイトから 4,065,536 バイトに引き下げています。それを超えたときに何が起こるかは ASP.NET Core でのアップロードのサイズ制限で説明しています。その後、ASP.NET Core のアップロードのガイドと同様に、RteFiles.Save がフォルダー、サイズ、拡張子、先頭バイトをチェックし、新しい名前でファイルを保存します。

Blazor のサンプルの RichTextEditor。アップロードされた夜の街の写真が本文に挿入されて選択されており、上部にはすべてのボタンを表示したツールバーがある
Blazor のページに挿入した、ギャラリーの画像。その URL は HTTP エンドポイントから返されたものです。

テストする

このガイドのサンプルプロジェクトを dotnet run で実行し、http://localhost:5304 を開きます。「Insert Gallery」を開いて「Upload」をクリックし、写真を選ぶと、写真は保存されて一覧に表示され、ダブルクリックすると挿入されます。シェルからも同じルートを使えます。

Shell
curl -F "file=@photo.jpg" http://localhost:5304/rte/upload
{"url":"/uploads/photo-e2f78227.jpg"}

ブラウザーのネットワークパネルでは、画像は _blazor の WebSocket と並ぶ通常の POST /rte/upload リクエストとして表示されます。

ほかのプラットフォーム向けの同じガイド

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5 · Web Forms

次に読む