ASP.NET Core Blazor ファイルのアップロード

この記事では、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 ファイルまたは複数ファイルの読み取りが 512,000 バイト (500 KB) を超えると、例外が発生します。 この制限により、開発者が誤って大きいファイルをメモリに読み取ることが防がれます。 OpenReadStreammaxAllowedSize パラメーターを使用することにより、必要に応じてさらに大きいサイズを指定できます。

ファイルのバイト数を表す Stream にアクセスする必要がある場合は、IBrowserFile.OpenReadStream を使用します。 受信ファイル ストリームをメモリに一度に直接読み取ることは避けてください。 たとえば、ファイルのすべてのバイトを MemoryStream にコピーしたり、ストリーム全体を一度にバイト配列に読み取ったりしないでください。 このような方法は、特に Blazor Server アプリの場合、パフォーマンスやセキュリティの問題が発生する可能性があります。 代わりに、次のいずれかの方法を使うことを検討してください。

  • ホステッド Blazor WebAssembly アプリまたは Blazor Server アプリのサーバーで、メモリに読み込むことなく、ディスク上のファイルにストリームを直接コピーします。 Blazor アプリではクライアントのファイル システムに直接アクセスできないことにご注意ください。
  • クライアントから外部サービスにファイルを直接アップロードします。 詳しくは、「ファイルを外部サービスにアップロードする」セクションをご覧ください。

次の例では、browserFile はアップロードされたファイルを表し、IBrowserFile を実装しています。

❌ 次の方法は、ファイルの Stream の内容がメモリ (reader) 内の String に読み込まれるため、推奨されません

var reader = 
    await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();

❌ 次の方法は、UploadBlobAsync を呼び出す前に、ファイルの Stream の内容がメモリ (memoryStream) 内の MemoryStream にコピーされるため、Microsoft Azure Blob Storage には推奨されません

var memoryStream = new MemoryStream();
browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
    trustedFilename, memoryStream));

✔️ 次の方法は、ファイルの Stream がコンシューマー (指定されたパスにファイルを作成する FileStream) に直接提供されるため、推奨されます

await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);

✔️ 次の方法は、ファイルの StreamUploadBlobAsync に直接提供されるため、Microsoft Azure Blob Storage に対して推奨されます

await blobContainerClient.UploadBlobAsync(
    trustedFilename, browserFile.OpenReadStream());

イメージ ファイルを受信するコンポーネントは、ファイルの便利な BrowserFileExtensions.RequestImageFileAsync メソッドを呼び出して、イメージがアプリにストリームされる前に、ブラウザーの JavaScript ランタイム内のイメージ データのサイズを変更できます。 RequestImageFileAsync を呼び出すためのユース ケースは、Blazor WebAssembly アプリに最も適しています。

次の例は、コンポーネントでの複数のファイルのアップロードを示しています。 InputFileChangeEventArgs.GetMultipleFiles では、複数のファイルを読み取ることができます。 悪意のあるユーザーがアプリで想定されているよりも多くのファイルをアップロードするのを防ぐため、ファイルの最大数を指定します。 ファイルのアップロードで複数のファイルがサポートされていない場合、InputFileChangeEventArgs.File を使用すると、最初のファイルのみを読み取ることができます。

Note

InputFileChangeEventArgsMicrosoft.AspNetCore.Components.Forms 名前空間にあります。これは、通常、アプリの _Imports.razor ファイル内の名前空間の 1 つです。 名前空間が _Imports.razor ファイル内に存在する場合、それにより API メンバーはアプリのコンポーネントにアクセスできます。

using Microsoft.AspNetCore.Components.Forms

_Imports.razor ファイル内の名前空間は、C# ファイル (.cs) には適用されません。 C# ファイルには、明示的な using ディレクティブが必要です。

Note

ファイル アップロード コンポーネントをテストする場合は、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)。

Pages/FileUpload1.razor:

@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 コンポーネントの例については、次のセクションをご覧ください。

@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 Server アプリから別のアプリ (場合によっては別のサーバー) 内のバックエンド Web API コントローラーに、ファイルをアップロードする方法を示します。

Blazor Server アプリにおいて、アプリで HttpClient インスタンスを作成できるようにする IHttpClientFactory と関連サービスを追加します。

Program.cs:

builder.Services.AddHttpClient();

詳細については、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」を参照してください。

このセクションの例では次のようになります。

  • Web API は次の URL で実行されます: https://localhost:5001
  • Blazor Server アプリは次の URL で実行されます: https://localhost:5003

テストの場合は、前記の URL をプロジェクトの Properties/launchSettings.json ファイルで構成します。

次の例では、ホストされている Blazor WebAssembly ソリューションServer アプリで、Web API コントローラーにファイルをアップロードする方法を示しています。

重要

ホストされている Blazor WebAssembly アプリを実行する場合は、ソリューションの Server プロジェクトから、そのアプリを実行します。

結果のクラスをアップロードする

次の UploadResult クラスは、アップロードされたファイルの結果を保持するために、クライアント プロジェクトと Web API プロジェクトに配置されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために 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; }
}

アップロードしたファイルの結果は、 Shared プロジェクトの次の UploadResult クラスによって保持されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName でクライアントに返されます。 ファイルは、FileName の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。 次の例では、プロジェクトの名前空間は BlazorSample.Shared です。

ホストされている Blazor WebAssembly ソリューションShared プロジェクト内にある UploadResult.cs

namespace BlazorSample.Shared
{
    public class UploadResult
    {
        public bool Uploaded { get; set; }
        public string? FileName { get; set; }
        public string? StoredFileName { get; set; }
        public int ErrorCode { get; set; }
    }
}

UploadResult クラスを Client プロジェクトで使用できるようにするには、Shared プロジェクト用に Client プロジェクトの _Imports.razor ファイルにインポートを追加します。

@using BlazorSample.Shared

注意

運用アプリのセキュリティのベスト プラクティスは、アプリ、サーバー、またはネットワークに関する機密情報を明らかにするおそれがあるエラー メッセージを、クライアントに送信しないようにすることです。 詳細なエラー メッセージを提供すると、アプリ、サーバー、またはネットワークを攻撃しようとしている悪意のあるユーザーの手助けをしてしまうことになります。 このセクションのコード例では、サーバー側でエラーが発生した場合、コンポーネントのクライアント側で表示するために、エラー コード番号 (int) のみが返送されます。 ユーザーは、ファイルのアップロードに関するサポートが必要な場合は、エラーの正確な原因を知ることなく、サポート チケットの解決のためにエラー コードをサポート担当者に提供します。

コンポーネントをアップロードする

次の FileUpload2 コンポーネントでは、次を実行します。

  • クライアントからファイルをアップロードすることをユーザーに許可します。
  • クライアントから提供された信頼できない、または安全ではないファイル名を、UI に表示します。 信頼できない、または安全ではないファイル名は、UI で安全に表示するために、Razor によって自動的に HTML でエンコードされます。

警告

次の目的には、クライアントから提供されたファイル名を信頼しないでください

  • ファイルをファイル システムまたはサービスに保存する。
  • ファイル名が自動的にエンコードされない UI に、または開発者コードを使用して表示する。

サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。

Blazor Server アプリでの Pages/FileUpload2.razor:

@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@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 * 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
                {
                    var fileContent = 
                        new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    files.Add(new() { Name = file.Name });

                    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; }
    }
}

Client プロジェクトの Pages/FileUpload2.razor:

@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 * 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
                {
                    var fileContent = 
                        new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    files.Add(new() { Name = file.Name });

                    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; }
    }
}

コントローラーをアップロードする

Web API プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。

次のコードを使用するには、Development 環境で実行されているアプリの Web API プロジェクトのルートに、Development/unsafe_uploads フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging 環境用には Staging/unsafe_uploads フォルダーを作成します。 Production 環境用には Production/unsafe_uploads フォルダーを作成します。

警告

この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。

Controllers/FilesaveController.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
    private readonly IWebHostEnvironment env;
    private readonly ILogger<FilesaveController> logger;

    public FilesaveController(IWebHostEnvironment env,
        ILogger<FilesaveController> logger)
    {
        this.env = env;
        this.logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = new();

        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);
    }
}

Server プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。

次のコードを使用するには、Development 環境で実行されているアプリの Server プロジェクトのルートに、Development/unsafe_uploads フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging 環境用には Staging/unsafe_uploads フォルダーを作成します。 Production 環境用には Production/unsafe_uploads フォルダーを作成します。

警告

この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。

Controllers/FilesaveController.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;

[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
    private readonly IWebHostEnvironment env;
    private readonly ILogger<FilesaveController> logger;

    public FilesaveController(IWebHostEnvironment env,
        ILogger<FilesaveController> logger)
    {
        this.env = env;
        this.logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = new();

        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 が呼び出され、安全なファイル名が生成されます。 ブラウザーによって提供されるファイル名を信頼しないでください。攻撃者は、既存のファイルを上書きする既存のファイル名を選択したり、アプリの外部で書き込みを試みるパスを送信したりする可能性があります。

進行状況を表示してファイルをアップロードする

次の例は、Blazor Server アプリで、アップロードの進行状況をユーザーに表示しながらファイルをアップロードする方法を示しています。

テスト アプリで以下の例を使用するには、次のようにします。

  • アップロードした Development 環境用のファイルを保存するためのフォルダーを作成します: Development/unsafe_uploads
  • 最大ファイル サイズ (maxFileSize、次の例では 15 MB) と、許可されるファイルの最大数 (maxAllowedFiles、次の例では 3) を構成します。
  • 必要に応じて、バッファーを別の値 (次の例では 10 KB) に設定して、進行状況を報告する頻度を増やします。 パフォーマンスおよびセキュリティ上の懸念があるため、30 KB を超えるバッファーの使用はお勧めしません。

Pages/FileUpload3.razor:

@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 * 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 コンポーネントではサポートされていません。

ファイル ストリーム

Blazor Server では、ファイルが読み取られるときに、ファイル データがサーバー上の .NET コードに SignalR 接続を介してストリームされます。

Blazor WebAssembly では、ファイル データはブラウザー内の .NET コードに直接ストリームされます。

ファイルを外部サービスにアップロードする

アプリでファイルのアップロード バイトを処理し、アプリのサーバーでアップロードされたファイルを受信する代わりに、クライアントから外部サービスにファイルを直接アップロードできます。 アプリは、必要に応じて外部サービスからファイルを安全に処理できます。 この方法により、悪意のある攻撃や潜在的なパフォーマンスの問題に対してアプリとそのサーバーが強化されます。

Azure FilesAzure Blob Storage、または次のような利点を持つサード パーティのサービスを使用するアプローチを検討してください。

Azure Blob Storage と Azure Files について詳しくは、Azure Storage のドキュメントをご覧ください。

その他のリソース

警告

ファイルのアップロードをユーザーに許可する場合は、常に、セキュリティのベスト プラクティスに従ってください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。

最大 2 GB のブラウザー ファイルのデータを .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 ファイルまたは複数ファイルの読み取りが 512,000 バイト (500 KB) を超えると、例外が発生します。 この制限により、開発者が誤って大きいファイルをメモリに読み取ることが防がれます。 OpenReadStreammaxAllowedSize パラメーターを使用すると、サポートされる最大サイズである 2 GB (2,147,483,648 バイト) まで、必要に応じて、より大きなサイズを指定できます。

Note

2 GB というフレームワーク ファイル サイズの制限は、ASP.NET Core 5.0 にのみ適用されます。 ASP.NET Core 6.0 以降、フレームワークでファイルの最大サイズは制限されません。

ファイルのバイト数を表す Stream にアクセスする必要がある場合は、IBrowserFile.OpenReadStream を使用します。 受信ファイル ストリームをメモリに一度に直接読み取ることは避けてください。 たとえば、ファイルのすべてのバイトを MemoryStream にコピーしたり、ストリーム全体を一度にバイト配列に読み取ったりしないでください。 このような方法は、特に Blazor Server アプリの場合、パフォーマンスやセキュリティの問題が発生する可能性があります。 代わりに、次のいずれかの方法を使うことを検討してください。

  • ストリームを、メモリに読み取るのではなく、ディスク上のファイルに直接コピーします。
  • クライアントから外部サービスにファイルを直接アップロードします。 詳しくは、「ファイルを外部サービスにアップロードする」セクションをご覧ください。

次の例では、browserFile はアップロードされたファイルを表し、IBrowserFile を実装しています。

❌ 次の方法は、ファイルの Stream の内容がメモリ (reader) 内の String に読み込まれるため、推奨されません

var reader = 
    await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();

❌ 次の方法は、UploadBlobAsync を呼び出す前に、ファイルの Stream の内容がメモリ (memoryStream) 内の MemoryStream にコピーされるため、Microsoft Azure Blob Storage には推奨されません

var memoryStream = new MemoryStream();
browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
    trustedFilename, memoryStream));

✔️ 次の方法は、ファイルの Stream がコンシューマー (指定されたパスにファイルを作成する FileStream) に直接提供されるため、推奨されます

await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);

✔️ 次の方法は、ファイルの StreamUploadBlobAsync に直接提供されるため、Microsoft Azure Blob Storage に対して推奨されます

await blobContainerClient.UploadBlobAsync(
    trustedFilename, browserFile.OpenReadStream());

イメージ ファイルを受信するコンポーネントは、ファイルの便利な BrowserFileExtensions.RequestImageFileAsync メソッドを呼び出して、イメージがアプリにストリームされる前に、ブラウザーの JavaScript ランタイム内のイメージ データのサイズを変更できます。 RequestImageFileAsync を呼び出すためのユース ケースは、Blazor WebAssembly アプリに最も適しています。

次の例は、コンポーネントでの複数のファイルのアップロードを示しています。 InputFileChangeEventArgs.GetMultipleFiles では、複数のファイルを読み取ることができます。 悪意のあるユーザーがアプリで想定されているよりも多くのファイルをアップロードするのを防ぐため、ファイルの最大数を指定します。 ファイルのアップロードで複数のファイルがサポートされていない場合、InputFileChangeEventArgs.File を使用すると、最初のファイルのみを読み取ることができます。

Note

InputFileChangeEventArgsMicrosoft.AspNetCore.Components.Forms 名前空間にあります。これは、通常、アプリの _Imports.razor ファイル内の名前空間の 1 つです。 名前空間が _Imports.razor ファイル内に存在する場合、それにより API メンバーはアプリのコンポーネントにアクセスできます。

using Microsoft.AspNetCore.Components.Forms

_Imports.razor ファイル内の名前空間は、C# ファイル (.cs) には適用されません。 C# ファイルには、明示的な using ディレクティブが必要です。

Note

ファイル アップロード コンポーネントをテストする場合は、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)。

Pages/FileUpload1.razor:

@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 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 Server アプリから別のアプリ (場合によっては別のサーバー) 内のバックエンド Web API コントローラーに、ファイルをアップロードする方法を示します。

Blazor Server アプリにおいて、アプリで HttpClient インスタンスを作成できるようにする IHttpClientFactory と関連サービスを追加します。

Startup.csStartup.ConfigureServices で:

services.AddHttpClient();

詳細については、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」を参照してください。

このセクションの例では次のようになります。

  • Web API は次の URL で実行されます: https://localhost:5001
  • Blazor Server アプリは次の URL で実行されます: https://localhost:5003

テストの場合は、前記の URL をプロジェクトの Properties/launchSettings.json ファイルで構成します。

次の例では、ホストされている Blazor WebAssembly ソリューションServer アプリで、Web API コントローラーにファイルをアップロードする方法を示しています。

重要

ホストされている Blazor WebAssembly アプリを実行する場合は、ソリューションの Server プロジェクトから、そのアプリを実行します。

結果のクラスをアップロードする

次の UploadResult クラスは、アップロードされたファイルの結果を保持するために、クライアント プロジェクトと Web API プロジェクトに配置されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために 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; }
}

アップロードしたファイルの結果は、 Shared プロジェクトの次の UploadResult クラスによって保持されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName でクライアントに返されます。 ファイルは、FileName の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。 次の例では、プロジェクトの名前空間は BlazorSample.Shared です。

ホストされている Blazor WebAssembly ソリューションShared プロジェクト内にある UploadResult.cs

namespace BlazorSample.Shared
{
    public class UploadResult
    {
        public bool Uploaded { get; set; }
        public string FileName { get; set; }
        public string StoredFileName { get; set; }
        public int ErrorCode { get; set; }
    }
}

UploadResult クラスを Client プロジェクトで使用できるようにするには、Shared プロジェクト用に Client プロジェクトの _Imports.razor ファイルにインポートを追加します。

@using BlazorSample.Shared

注意

運用アプリのセキュリティのベスト プラクティスは、アプリ、サーバー、またはネットワークに関する機密情報を明らかにするおそれがあるエラー メッセージを、クライアントに送信しないようにすることです。 詳細なエラー メッセージを提供すると、アプリ、サーバー、またはネットワークを攻撃しようとしている悪意のあるユーザーの手助けをしてしまうことになります。 このセクションのコード例では、サーバー側でエラーが発生した場合、コンポーネントのクライアント側で表示するために、エラー コード番号 (int) のみが返送されます。 ユーザーは、ファイルのアップロードに関するサポートが必要な場合は、エラーの正確な原因を知ることなく、サポート チケットの解決のためにエラー コードをサポート担当者に提供します。

コンポーネントをアップロードする

次の FileUpload2 コンポーネントでは、次を実行します。

  • クライアントからファイルをアップロードすることをユーザーに許可します。
  • クライアントから提供された信頼できない、または安全ではないファイル名を、UI に表示します。 信頼できない、または安全ではないファイル名は、UI で安全に表示するために、Razor によって自動的に HTML でエンコードされます。

警告

次の目的には、クライアントから提供されたファイル名を信頼しないでください

  • ファイルをファイル システムまたはサービスに保存する。
  • ファイル名が自動的にエンコードされない UI に、または開発者コードを使用して表示する。

サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。

Blazor Server アプリでの Pages/FileUpload2.razor:

@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@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 * 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
                {
                    var fileContent = 
                        new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    files.Add(new() { Name = file.Name });

                    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);

                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; }
    }
}

Client プロジェクトの Pages/FileUpload2.razor:

@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 * 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
                {
                    var fileContent = 
                        new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    files.Add(new() { Name = file.Name });

                    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; }
    }
}

コントローラーをアップロードする

Web API プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。

次のコードを使用するには、Development 環境で実行されているアプリの Web API プロジェクトのルートに、Development/unsafe_uploads フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging 環境用には Staging/unsafe_uploads フォルダーを作成します。 Production 環境用には Production/unsafe_uploads フォルダーを作成します。

警告

この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。

Controllers/FilesaveController.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
    private readonly IWebHostEnvironment env;
    private readonly ILogger<FilesaveController> logger;

    public FilesaveController(IWebHostEnvironment env,
        ILogger<FilesaveController> logger)
    {
        this.env = env;
        this.logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = new();

        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);
    }
}

Server プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。

次のコードを使用するには、Development 環境で実行されているアプリの Server プロジェクトのルートに、Development/unsafe_uploads フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging 環境用には Staging/unsafe_uploads フォルダーを作成します。 Production 環境用には Production/unsafe_uploads フォルダーを作成します。

警告

この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。

Controllers/FilesaveController.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;

[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
    private readonly IWebHostEnvironment env;
    private readonly ILogger<FilesaveController> logger;

    public FilesaveController(IWebHostEnvironment env,
        ILogger<FilesaveController> logger)
    {
        this.env = env;
        this.logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = new();

        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);
    }
}

進行状況を表示してファイルをアップロードする

次の例は、Blazor Server アプリで、アップロードの進行状況をユーザーに表示しながらファイルをアップロードする方法を示しています。

テスト アプリで以下の例を使用するには、次のようにします。

  • アップロードした Development 環境用のファイルを保存するためのフォルダーを作成します: Development/unsafe_uploads
  • 最大ファイル サイズ (maxFileSize、次の例では 15 MB) と、許可されるファイルの最大数 (maxAllowedFiles、次の例では 3) を構成します。
  • 必要に応じて、バッファーを別の値 (次の例では 10 KB) に設定して、進行状況を報告する頻度を増やします。 パフォーマンスおよびセキュリティ上の懸念があるため、30 KB を超えるバッファーの使用はお勧めしません。

Pages/FileUpload3.razor:

@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 * 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 コンポーネントではサポートされていません。

ファイル ストリーム

Blazor Server では、ファイルがストリームから読み取られるときに、ファイル データがサーバー上の .NET コードに SignalR 接続を介してストリームされます。 RemoteBrowserFileStreamOptions では、Blazor Server のファイル アップロード特性を構成することができます。

Blazor WebAssembly では、ファイル データはブラウザー内の .NET コードに直接ストリームされます。

ファイルを外部サービスにアップロードする

アプリでファイルのアップロード バイトを処理し、アプリのサーバーでアップロードされたファイルを受信する代わりに、クライアントから外部サービスにファイルを直接アップロードできます。 アプリは、必要に応じて外部サービスからファイルを安全に処理できます。 この方法により、悪意のある攻撃や潜在的なパフォーマンスの問題に対してアプリとそのサーバーが強化されます。

Azure FilesAzure Blob Storage、または次のような利点を持つサード パーティのサービスを使用するアプローチを検討してください。

Azure Blob Storage と Azure Files について詳しくは、Azure Storage のドキュメントをご覧ください。

その他のリソース