ASP.NET Core Blazor ファイルのアップロード
注意
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
この記事では、InputFile コンポーネントを使用して Blazor 内のファイルをアップロードする方法について説明します。
ファイルのアップロード
警告
ファイルのアップロードをユーザーに許可する場合は、常に、セキュリティのベスト プラクティスに従ってください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
ブラウザー ファイルのデータを .NET コードに読み込むには、InputFile コンポーネントを使用します。 InputFile コンポーネントにより、単一のファイルアップロードのために file
型の HTML <input>
要素がレンダリングされます。 multiple
属性を追加して、ユーザーが一度に複数のファイルをアップロードできるようにします。
InputFile コンポーネントまたはその基礎 HTML <input type="file">
を使用するとき、ファイル選択は累積されません。そのため、ファイルは既存のファイル選択に追加できません。 このコンポーネントでは常にユーザーの最初のファイル選択が置換されます。そのため、前の選択からのファイル参照は利用できません。
OnChange (change
) イベントが発生すると、次の InputFile コンポーネントによって LoadFiles
メソッドが実行されます。 InputFileChangeEventArgs により、選択されているファイルの一覧と各ファイルの詳細にアクセスできます。
<InputFile OnChange="LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
レンダリングされる HTML:
<input multiple="" type="file" _bl_2="">
Note
前の例では、<input>
要素の _bl_2
属性が、Blazor の内部処理に使用されます。
ユーザーが選択したファイルからデータを読み取るには、ファイルで IBrowserFile.OpenReadStream を呼び出し、返されるストリームから読み取ります。 詳細については、「ファイル ストリーム」セクションを参照してください。
OpenReadStream により、Stream の最大サイズ (バイト単位) が適用されます。 1 ファイルまたは複数ファイルの読み取りが 500 KB を超えると、例外が発生します。 この制限により、開発者が誤って大きいファイルをメモリに読み取ることが防がれます。 OpenReadStream の maxAllowedSize
パラメーターを使用することにより、必要に応じてさらに大きいサイズを指定できます。
ファイルのバイト数を表す Stream にアクセスする必要がある場合は、IBrowserFile.OpenReadStream を使用します。 受信ファイル ストリームをメモリに一度に直接読み取ることは避けてください。 たとえば、ファイルのすべてのバイトを MemoryStream にコピーしたり、ストリーム全体を一度にバイト配列に読み取ったりしないでください。 これらのアプローチにより、アプリのパフォーマンスが低下し、特にサーバー側コンポーネントのサービス拒否 (DoS) のリスクが発生する可能性があります。 代わりに、次のいずれかの方法を使うことを検討してください。
- ストリームを、メモリに読み取るのではなく、ディスク上のファイルに直接コピーします。 サーバーでコードを実行する Blazor アプリでは、クライアントのファイル システムに直接アクセスできないことにご注意ください。
- クライアントから外部サービスにファイルを直接アップロードします。 詳しくは、「ファイルを外部サービスにアップロードする」セクションをご覧ください。
次の例では、browserFile
はアップロードされたファイルを表し、IBrowserFile を実装しています。 IBrowserFile の機能する実装は、この記事で後述するファイル アップロード コンポーネントで示されています。
サポート: 次の方法は、ファイルの Stream がコンシューマー (指定されたパスにファイルを作成する FileStream) に直接提供されるため、推奨されます。
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
サポート: 次の方法は、ファイルの Stream が UploadBlobAsync に直接提供されるため、Microsoft Azure Blob Storage に対して推奨されます。
await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream());
非推奨: 次の方法は、ファイルの Stream の内容がメモリ内の String (reader
) に読み込まれるため、推奨されません。
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
非推奨: 次の方法は、UploadBlobAsync を呼び出す前に、ファイルの Stream の内容がメモリ内の MemoryStream (memoryStream
) にコピーされるため、Microsoft Azure Blob Storage には推奨されません。
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));
イメージ ファイルを受信するコンポーネントは、ファイルの便利な BrowserFileExtensions.RequestImageFileAsync メソッドを呼び出して、イメージがアプリにストリームされる前に、ブラウザーの JavaScript ランタイム内のイメージ データのサイズを変更できます。 RequestImageFileAsync を呼び出すためのユース ケースは、Blazor WebAssembly アプリに最も適しています。
Autofac Inversion of Control (IoC) コンテナー ユーザー
組み込みの ASP.NET Core 依存関係挿入コンテナーではなく、Autofac Inversion of Control (IoC) コンテナーを使用している場合、サーバー側回線ハンドラー ハブ オプションで true
に DisableImplicitFromServicesParameters を設定します。 詳細については、「FileUpload: Did not receive any data in the allotted time (dotnet/aspnetcore
#38842)」をご覧ください。
ファイルの読み取りとアップロードのサイズ制限
サーバー側の場合もクライアント側の場合でも、InputFile コンポーネントに特別なファイル読み取りやアップロードのサイズ制限はありません。 ただし、クライアント側の Blazor では、JavaScript から C# にデータをマーシャリングするときに、ファイルのバイトが 1 つの JavaScript 配列バッファーに読み取られます。これは、2 GB またはデバイスの使用可能なメモリに制限されます。 大容量ファイルのアップロード (> 250 MB) では、InputFile コンポーネントを使用するクライアント側のアップロードに失敗する可能性があります。 詳しくは、次のディスカッションを参照してください。
InputFile コンポーネントでサポートされる最大ファイル サイズは 2 GB です。 さらに、クライアント側の Blazor では、JavaScript から C# にデータをマーシャリングするときに、ファイルのバイトが 1 つの JavaScript 配列バッファーに読み取られます。これは、2 GB またはデバイスの使用可能なメモリに制限されます。 大容量ファイルのアップロード (> 250 MB) では、InputFile コンポーネントを使用するクライアント側のアップロードに失敗する可能性があります。 詳しくは、次のディスカッションを参照してください。
- Blazor InputFile コンポーネントは、ファイルのアップロード時にチャンクを処理する必要があります (dotnet/ランタイム #84685)。
- http ハンドラーを介したストリーミング アップロードの要求 (dotnet/ランタイム #36634)
InputFile コンポーネントを使用しようとすると失敗する大容量のクライアント側ファイルをアップロードする場合は、InputFile コンポーネントを使用する代わりに、複数のHTTP 範囲要求を使用してカスタム コンポーネントで大容量ファイルをチャンク化することをお勧めします。
現在、.NET 9 (2024 年後半) でクライアント側のファイル サイズ アップロード制限への対応作業が予定されています。
例
次の例は、コンポーネントでの複数のファイルのアップロードを示しています。 InputFileChangeEventArgs.GetMultipleFiles では、複数のファイルを読み取ることができます。 悪意のあるユーザーがアプリで想定されているよりも多くのファイルをアップロードするのを防ぐため、ファイルの最大数を指定します。 ファイルのアップロードで複数のファイルがサポートされていない場合、InputFileChangeEventArgs.File を使用すると、最初のファイルのみを読み取ることができます。
InputFileChangeEventArgs は Microsoft.AspNetCore.Components.Forms 名前空間にあります。これは、通常、アプリの _Imports.razor
ファイル内の名前空間の 1 つです。 名前空間が _Imports.razor
ファイル内に存在する場合、それにより API メンバーはアプリのコンポーネントにアクセスできます。
_Imports.razor
ファイル内の名前空間は、C# ファイル (.cs
) には適用されません。 C# ファイルでは、以下のクラス ファイルの先頭に using
ディレクティブを明示的に記述する必要があります:
using Microsoft.AspNetCore.Components.Forms;
ファイル アップロード コンポーネントをテストする場合は、PowerShell を使用して任意のサイズのテスト ファイルを作成できます。
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
上記のコマンドでは次のことが行われます。
{SIZE}
プレースホルダーでは、ファイルのサイズはバイト単位で表します (たとえば、2 MB のファイルの場合は2097152
)。{PATH}
プレースホルダーは、ファイル拡張子を持つパスとファイルです (例:D:/test_files/testfile2MB.txt
)。
サーバー側のファイルのアップロード例
次のコードを使用するには、Development
環境で実行されているアプリのルートに、Development/unsafe_uploads
フォルダーを作成します。
この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、ファイルの内容をスキャンせずに保存しています。この記事のガイダンスでは、アップロードされたファイルのセキュリティに関する追加のベスト プラクティスを考慮していません。 ステージング システムと運用システムでは、アップロード フォルダーに対する実行アクセス許可を無効にし、アップロード直後にウイルス対策またはマルウェア対策スキャナー API を使ってファイルをスキャンしてください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileName);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
クライアント側のファイルのアップロード例
次の例では、ファイル バイトが処理され、アプリの外の宛先にファイルが送信されることはありません。 ファイルをサーバーまたはサービスに送信する Razor コンポーネントの例については、次のセクションをご覧ください。
コンポーネントでは、対話型 WebAssembly レンダリング モード (InteractiveWebAssembly
) が親コンポーネントから継承されるか、アプリにグローバルに適用されるものと想定されています。
@page "/file-upload-1"
@inject ILogger<FileUpload1> Logger
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
IBrowserFile は、ブラウザーによって公開されるメタデータをプロパティとして返します。 このメタデータは、事前検証に使用します。
次のプロパティ (具体的には、UI に表示される Name プロパティ) の値は信頼しないでください。ユーザー指定のデータはすべて、アプリ、サーバー、ネットワークに対する重要なセキュリティ リスクとして扱います。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
サーバー側のレンダリングを使用するサーバーへのファイルのアップロード
このセクションは、Blazor Web App または Blazor Server アプリの対話型サーバー コンポーネントに適用されます。
次の例では、サーバー側のアプリから別のアプリ (場合によっては別のサーバー) 内のバックエンド Web API コントローラーに、ファイルをアップロードする方法を示します。
サーバー側のアプリの Program
ファイルにおいて、アプリで HttpClient インスタンスを作成できるようにする IHttpClientFactory と関連サービスを追加します。
builder.Services.AddHttpClient();
詳細については、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」を参照してください。
このセクションの例では次のようになります。
- Web API は次の URL で実行されます:
https://localhost:5001
- サーバー側のアプリは次の URL で実行されます:
https://localhost:5003
テストの場合は、前記の URL をプロジェクトの Properties/launchSettings.json
ファイルで構成します。
アップロードしたファイルの結果は、次の UploadResult
クラスによって保持されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode
でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName
でクライアントに返されます。 ファイルは、FileName
の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
運用アプリのセキュリティのベスト プラクティスは、アプリ、サーバー、またはネットワークに関する機密情報を明らかにするおそれがあるエラー メッセージを、クライアントに送信しないようにすることです。 詳細なエラー メッセージを提供すると、アプリ、サーバー、またはネットワークを攻撃しようとしている悪意のあるユーザーの手助けをしてしまうことになります。 このセクションのコード例では、サーバー側でエラーが発生した場合、コンポーネントのクライアント側で表示するために、エラー コード番号 (int
) のみが返送されます。 ユーザーは、ファイルのアップロードに関するサポートが必要な場合は、エラーの正確な原因を知ることなく、サポート チケットの解決のためにエラー コードをサポート担当者に提供します。
次の LazyBrowserFileStream
クラスは、ストリームの最初のバイトが要求される直前に、OpenReadStream を遅延的に呼び出すカスタム ストリーム型を定義します。 ストリームは、.NET でストリームの読み取りが開始されるまで、ブラウザーからサーバーに送信されません。
LazyBrowserFileStream.cs
:
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
: Stream
{
private readonly IBrowserFile file = file;
private readonly int maxAllowedSize = maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public override void Flush() => underlyingStream?.Flush();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen() =>
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
次の FileUpload2
コンポーネントでは、次を実行します。
- クライアントからファイルをアップロードすることをユーザーに許可します。
- クライアントから提供された信頼できない、または安全ではないファイル名を、UI に表示します。 信頼できない、または安全ではないファイル名は、UI で安全に表示するために、Razor によって自動的に HTML でエンコードされます。
警告
次の目的には、クライアントから提供されたファイル名を信頼しないでください。
- ファイルをファイル システムまたはサービスに保存する。
- ファイル名が自動的にエンコードされない UI に、または開発者コードを使用して表示する。
サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Any())
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
コンポーネントがファイルのアップロードを一度に 1 つのファイルに制限する場合、またはコンポーネントが対話型クライアント側レンダリング (CSR、InteractiveWebAssembly
) のみを採用している場合、コンポーネントは LazyBrowserFileStream
の使用を回避し、Stream を使用できます。 次に、FileUpload2
コンポーネントの変更を示します。
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
使用されていないため、LazyBrowserFileStream
クラス (LazyBrowserFileStream.cs
) を削除します。
コンポーネントがファイルのアップロードを一度に 1 つのファイルに制限する場合、コンポーネントは LazyBrowserFileStream
の使用を回避し、Stream を使用できます。 次に、FileUpload2
コンポーネントの変更を示します。
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
使用されていないため、LazyBrowserFileStream
クラス (LazyBrowserFileStream.cs
) を削除します。
Web API プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。
重要
このセクションのコントローラーは、Blazor アプリとは別の Web API プロジェクトで使用することを目的としています。 ファイル アップロード ユーザーが認証された場合、Web API ではクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を軽減する必要があります。
Note
[FromForm]
属性を使ったフォーム値のバインドは、.NET 6.0 の ASP.NET Core の Minimal API ではネイティブにサポートされていません。 したがって、次の Filesave
コントローラーの例を、Minimal API を使用するように変換することはできません。 Minimal API を使用したフォーム値からのバインドのサポートは、.NET 7 以降の ASP.NET Core で利用できます。
次のコードを使用するには、Development
環境で実行されているアプリの Web API プロジェクトのルートに、Development/unsafe_uploads
フォルダーを作成します。
この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、ファイルの内容をスキャンせずに保存しています。この記事のガイダンスでは、アップロードされたファイルのセキュリティに関する追加のベスト プラクティスを考慮していません。 ステージング システムと運用システムでは、アップロード フォルダーに対する実行アクセス許可を無効にし、アップロード直後にウイルス対策またはマルウェア対策スキャナー API を使ってファイルをスキャンしてください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
Controllers/FilesaveController.cs
:
using System.Net;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
前述のコードで、GetRandomFileName が呼び出され、安全なファイル名が生成されます。 ブラウザーによって提供されるファイル名を信頼しないでください。攻撃者は、既存のファイルを上書きする既存のファイル名を選択したり、アプリの外部で書き込みを試みるパスを送信したりする可能性があります。
サーバー アプリでは、コントローラー サービスを登録してコントローラー エンドポイントをマップする必要があります。 詳細については、「ASP.NET Core でのコントローラー アクションへのルーティング」を参照してください。
クライアント側のレンダリング (CSR) を使用するサーバーへのファイルのアップロード
このセクションは、Blazor Web App または Blazor WebAssembly アプリのクライアント側レンダリング (CSR) コンポーネントに適用されます。
次の例は、CSR を採用している Blazor Web App のコンポーネントまたは Blazor WebAssembly アプリのコンポーネントから、別のアプリ (別のサーバー上の場合もあり) のバックエンド Web API コントローラーにファイルをアップロードする方法を示しています。
アップロードしたファイルの結果は、次の UploadResult
クラスによって保持されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode
でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName
でクライアントに返されます。 ファイルは、FileName
の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
Note
前述の UploadResult
クラスは、クライアント ベースのプロジェクトとサーバー ベースのプロジェクト間で共有できます。 クライアントのプロジェクトとサーバーのプロジェクトでクラスを共有する場合は、各プロジェクトの _Imports.razor
ファイルに共有プロジェクト用のインポートを追加します。 次に例を示します。
@using BlazorSample.Shared
次の FileUpload2
コンポーネントでは、次を実行します。
- クライアントからファイルをアップロードすることをユーザーに許可します。
- クライアントから提供された信頼できない、または安全ではないファイル名を、UI に表示します。 信頼できない、または安全ではないファイル名は、UI で安全に表示するために、Razor によって自動的に HTML でエンコードされます。
運用アプリのセキュリティのベスト プラクティスは、アプリ、サーバー、またはネットワークに関する機密情報を明らかにするおそれがあるエラー メッセージを、クライアントに送信しないようにすることです。 詳細なエラー メッセージを提供すると、アプリ、サーバー、またはネットワークを攻撃しようとしている悪意のあるユーザーの手助けをしてしまうことになります。 このセクションのコード例では、サーバー側でエラーが発生した場合、コンポーネントのクライアント側で表示するために、エラー コード番号 (int
) のみが返送されます。 ユーザーは、ファイルのアップロードに関するサポートが必要な場合は、エラーの正確な原因を知ることなく、サポート チケットの解決のためにエラー コードをサポート担当者に提供します。
警告
次の目的には、クライアントから提供されたファイル名を信頼しないでください。
- ファイルをファイル システムまたはサービスに保存する。
- ファイル名が自動的にエンコードされない UI に、または開発者コードを使用して表示する。
サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Blazor Web App のメイン プロジェクトで、プロジェクトの Program
ファイルに IHttpClientFactory および関連するサービスを追加します。
builder.Services.AddHttpClient();
クライアント側コンポーネントはサーバーでプリレンダリングされるため、HttpClient
サービスをメイン プロジェクトに追加する必要があります。 次のコンポーネントのプリレンダリングを無効にする場合、メイン アプリで HttpClient
サービスを提供する必要はなく、メイン プロジェクトに前述の行を追加する必要はありません。
ASP.NET Core アプリへの HttpClient
サービスの追加について詳しくは、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」をご覧ください。
Blazor Web App のクライアント プロジェクト (.Client
) では、バックエンド Web API コントローラーに対する HTTP POST 要求の HttpClient も登録する必要があります。 クライアント プロジェクトの Program
ファイルで次を確認するか、追加します。
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
上記の例では、ベース アドレスの設定に builder.HostEnvironment.BaseAddress
(IWebAssemblyHostEnvironment.BaseAddress) を使っています。これを使って、アプリのベース アドレス (通常は、ホスト ページの <base>
タグの href
値に由来します) を取得できます。 外部 Web API を呼び出している場合は、URI を Web API のベース アドレスに設定します。
Blazor Web App の次のコンポーネントの先頭で、対話型 WebAssembly レンダリング モード属性を指定します。
@rendermode InteractiveWebAssembly
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName);
if (result is null)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result = new();
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
サーバー側プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。
Note
[FromForm]
属性を使ったフォーム値のバインドは、.NET 6.0 の ASP.NET Core の Minimal API ではネイティブにサポートされていません。 したがって、次の Filesave
コントローラーの例を、Minimal API を使用するように変換することはできません。 Minimal API を使用したフォーム値からのバインドのサポートは、.NET 7 以降の ASP.NET Core で利用できます。
次のコードを使用するには、Development
環境で実行されているアプリのサーバー側プロジェクトのルートに、Development/unsafe_uploads
フォルダーを作成します。
この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、ファイルの内容をスキャンせずに保存しています。この記事のガイダンスでは、アップロードされたファイルのセキュリティに関する追加のベスト プラクティスを考慮していません。 ステージング システムと運用システムでは、アップロード フォルダーに対する実行アクセス許可を無効にし、アップロード直後にウイルス対策またはマルウェア対策スキャナー API を使ってファイルをスキャンしてください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
次の例では、共有プロジェクトで UploadResult
クラスが指定されている場合に、共有プロジェクトの名前空間を共有プロジェクトと一致するように更新します。
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
前述のコードで、GetRandomFileName が呼び出され、安全なファイル名が生成されます。 ブラウザーによって提供されるファイル名を信頼しないでください。攻撃者は、既存のファイルを上書きする既存のファイル名を選択したり、アプリの外部で書き込みを試みるパスを送信したりする可能性があります。
サーバー アプリでは、コントローラー サービスを登録してコントローラー エンドポイントをマップする必要があります。 詳細については、「ASP.NET Core でのコントローラー アクションへのルーティング」を参照してください。
ファイルのアップロードを取り消す
ファイル アップロード コンポーネントは、IBrowserFile.OpenReadStream または StreamReader.ReadAsync を呼び出すときに CancellationToken を使ってユーザーがアップロードを取り消したタイミングを検出できます。
InputFile
コンポーネントの CancellationTokenSource を作成します。 OnInputFileChange
メソッドの開始時に、以前のアップロードが進行中であるかどうかを確認します。
ファイルのアップロードが進行中の場合:
- 前のアップロードに対して Cancel を呼び出します。
- 次のアップロード用に新しい CancellationTokenSource を作成し、CancellationTokenSource.Token を OpenReadStream または ReadAsync に渡します。
進行中のサーバー側のファイルのアップロード
次の例は、サーバー側のアプリで、アップロードの進行状況をユーザーに表示しながらファイルをアップロードする方法を示しています。
テスト アプリで以下の例を使用するには、次のようにします。
- アップロードした
Development
環境用のファイルを保存するためのフォルダーを作成します:Development/unsafe_uploads
。 - 最大ファイル サイズ (
maxFileSize
、次の例では 15 KB) と、許可されるファイルの最大数 (maxAllowedFiles
、次の例では 3) を構成します。 - 必要に応じて、バッファーを別の値 (次の例では 10 KB) に設定して、進行状況を報告する頻度を増やします。 パフォーマンスおよびセキュリティ上の懸念があるため、30 KB を超えるバッファーの使用はお勧めしません。
警告
この例では、ファイルの内容をスキャンせずに保存しています。この記事のガイダンスでは、アップロードされたファイルのセキュリティに関する追加のベスト プラクティスを考慮していません。 ステージング システムと運用システムでは、アップロード フォルダーに対する実行アクセス許可を無効にし、アップロード直後にウイルス対策またはマルウェア対策スキャナー API を使ってファイルをスキャンしてください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
FileUpload3.razor
:
@page "/file-upload-3"
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 3</PageTitle>
<h1>File Upload Example 3</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
詳細については、次の API リソースを参照してください。
- FileStream: 同期および非同期両方の読み取り操作と書き込み操作をサポートするファイル用の Stream を提供します。
- FileStream.ReadAsync: 上記の
FileUpload3
コンポーネントは、ReadAsync を使用して非同期にストリームを読み取ります。 Read を使用してストリームを同期的に読み取る操作は、Razor コンポーネントではサポートされていません。
ファイル ストリーム
サーバーの対話機能を使用して、ファイルが読み取られるときに、ファイル データがサーバー上の .NET コードに SignalR 接続を介してストリーミングされます。
RemoteBrowserFileStreamOptions では、ファイルのアップロード特性を構成できます。
WebAssembly でレンダリングされるコンポーネントの場合、ファイル データはブラウザー内の .NET コードに直接ストリーミングされます。
アップロード画像のプレビュー
アップロード画像の画像をプレビューするには、まず、コンポーネント参照と OnChange
ハンドラーを InputFile
含むコンポーネントを追加します。
<InputFile @ref="inputFile" OnChange="ShowPreview" />
要素参照を含む画像要素を追加します。これは、画像プレビューのためのプレースホルダーとして機能します。
<img @ref="previewImageElem" />
関連付けられている参照を追加します。
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}
JavaScript で、HTML の input
要素と img
要素を指定して呼び出され、以下を実行する関数を追加します。
- 選択されたファイルを抽出する。
createObjectURL
を使用してオブジェクト URL を作成する。- 画像の読み込み後に
revokeObjectURL
でオブジェクト URL を取り消すイベント リスナーを設定し、メモリがリークされないようにする。 img
要素のソースを設定して画像を表示する。
window.previewImage = (inputElem, imgElem) => {
const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
imgElem.src = url;
}
最後に、挿入された IJSRuntime を使用して、JavaScript 関数を呼び出す OnChange
ハンドラーを追加します。
@inject IJSRuntime JS
...
@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
上記の例は、1 つの画像をアップロードする場合のものです。 この方法を拡張して、multiple
画像をサポートできます。
次の FileUpload4
コンポーネントは、完全な例を示しています。
FileUpload4.razor
:
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
ファイルを外部サービスにアップロードする
アプリでファイルのアップロード バイトを処理し、アプリのサーバーでアップロードされたファイルを受信する代わりに、クライアントから外部サービスにファイルを直接アップロードできます。 アプリは、必要に応じて外部サービスからファイルを安全に処理できます。 この方法により、悪意のある攻撃や潜在的なパフォーマンスの問題に対してアプリとそのサーバーが強化されます。
Azure Files、Azure Blob Storage、または次のような利点を持つサード パーティのサービスを使用するアプローチを検討してください。
- JavaScript クライアント ライブラリまたは REST API を使用して、クライアントから外部サービスにファイルを直接アップロードします。 たとえば、Azure には次のクライアント ライブラリと API が用意されています。
- クライアントによるファイルのアップロードごとにアプリ (サーバー側) によって生成されるユーザー委任の Shared Access Signature (SAS) トークンを使って、ユーザーのアップロードを承認します。 たとえば、Azure には次の SAS 機能があります。
- 自動冗長性とファイル共有のバックアップを提供します。
- クォータでアップロードを制限します。 Azure Blob Storage のクォータは、コンテナー レベルではなくアカウント レベルで設定されることに注意してください。 一方、Azure Files のクォータはファイル共有レベルであり、アップロードの制限をより適切に制御できる場合があります。 詳しくは、この一覧で前にリンクを示した Azure のドキュメントをご覧ください。
- サーバー側暗号化 (SSE) でファイルをセキュリティ保護します。
Azure Blob Storage と Azure Files について詳しくは、Azure Storage のドキュメントをご覧ください。
サーバー側の SignalR メッセージ サイズの制限
Blazor によって SignalR の最大メッセージ サイズを超えるファイルに関するデータが取得される場合、ファイルのアップロードは開始前でも失敗する可能性があります。
SignalR では、Blazor が受信するすべてのメッセージに適用されるメッセージ サイズ制限が定義されます。InputFile コンポーネントでは、構成された制限が適用されるメッセージでサーバーにファイルがストリーミングされます。 ただし、アップロードするファイルのセットを示す最初のメッセージは、一意の 1 つのメッセージとして送信されます。 最初のメッセージのサイズは、SignalR のメッセージ サイズの制限を超える可能性があります。 この問題はファイルのサイズとは関係ありません。ファイルの数に関係しています。
ログに記録されたエラーは、次のようになります。
エラー :次のエラーで接続が切断されました。"エラー: サーバーが終了時にエラーを返しました:接続はエラーで終了しました。" e.log @ blazor.server.js:1
ファイルをアップロードする場合、最初のメッセージでメッセージ サイズの制限に達することはまれです。 制限に達した場合、アプリでは HubOptions.MaximumReceiveMessageSize をより大きな値に構成できます。
SignalR の構成と MaximumReceiveMessageSize の設定方法について詳しくは、ASP.NET Core BlazorSignalR ガイダンスを参照してください。
クライアント ハブごとの最大並列呼び出しの設定
Blazor は既定値である 1 に設定された MaximumParallelInvocationsPerClient を利用します。
値を大きくすると、CopyTo
操作で System.InvalidOperationException: 'Reading is not allowed after reader was completed.'
がスローされる可能性が高くなります。 詳細については、「MaximumParallelInvocationsPerClient > 1 によって Blazor Server モードでのファイル アップロードが中断される (dotnet/aspnetcore
#53951)」を参照してください。
トラブルシューティング
IBrowserFile.OpenReadStream を呼び出す行は、System.TimeoutException をスローします。
System.TimeoutException: Did not receive any data in the allotted time.
考えられる原因:
組み込みの ASP.NET Core 依存関係挿入コンテナーの代わりに Autofac Inversion of Control (IoC) コンテナーを使用します。 この問題を解決するには、DisableImplicitFromServicesParameters をサーバー側回線ハンドラー ハブ オプションで
true
に設定します。 詳細については、「FileUpload: Did not receive any data in the allotted time (dotnet/aspnetcore
#38842)」をご覧ください。ストリームを最後まで読み取っていません。 これはフレームワークの問題ではありません。 例外をトラップし、ローカル環境/ネットワークでさらに調査します。
- サーバー側のレンダリングを使用し、複数のファイルで OpenReadStream を呼び出してから、完了まで読み取ります。 この問題を解決するには、この記事の「Upload files to a server with server-side rendering」セクションで説明されている
LazyBrowserFileStream
クラスとアプローチを使用します。
その他のリソース
ASP.NET Core