Bagikan melalui


Unggahan File di ASP.NET Core Blazor

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 9 dari artikel ini.

Peringatan

Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 9 dari artikel ini.

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 9 dari artikel ini.

Artikel ini menjelaskan cara mengunggah file dalam Blazor dengan komponen InputFile.

Unggahan file

Peringatan

Selalu ikuti praktik terbaik keamanan saat mengizinkan pengguna mengunggah file. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.

InputFile Gunakan komponen untuk membaca data file browser ke dalam kode .NET. Komponen InputFile merender elemen HTML <input> jenis file untuk unggahan file tunggal. multiple Tambahkan atribut untuk mengizinkan pengguna mengunggah beberapa file sekaligus.

Pemilihan file tidak bersifat kumulatif ketika menggunakan InputFile komponen atau HTML <input type="file"> yang mendasarinya, sehingga Anda tidak dapat menambahkan file ke seleksi file yang sudah ada. Komponen selalu menggantikan pilihan file awal pengguna, sehingga referensi file dari pilihan sebelumnya tidak tersedia.

Komponen berikut InputFile mengeksekusi metode LoadFiles ketika peristiwa OnChange (change) terjadi. Sebuah InputFileChangeEventArgs menyediakan akses ke daftar file yang dipilih dan detail tentang setiap file.

<InputFile OnChange="LoadFiles" multiple />

@code {
    private void LoadFiles(InputFileChangeEventArgs e)
    {
        ...
    }
}

HTML yang dirender:

<input multiple="" type="file" _bl_2="">

Catatan

Dalam contoh sebelumnya, <input> atribut elemen _bl_2 digunakan untuk Blazorpemrosesan internal.

Untuk membaca data dari file yang dipilih oleh pengguna, di mana Stream mewakili byte dari file tersebut, panggil IBrowserFile.OpenReadStream pada file dan bacalah dari aliran yang dihasilkan. Untuk informasi selengkapnya, lihat bagian Stream Berkas.

OpenReadStream memberlakukan ukuran maksimum dalam byte untuk Stream. Membaca satu file atau beberapa file yang lebih besar dari 500 KB menghasilkan pengecualian. Batas ini mencegah pengembang membaca file besar secara tidak sengaja ke dalam memori. Parameter maxAllowedSizeOpenReadStream dapat digunakan untuk menentukan ukuran yang lebih besar jika diperlukan.

Di luar pemrosesan file kecil, hindari membaca aliran file masuk langsung ke memori sekaligus. Misalnya, jangan menyalin semua byte file ke dalam MemoryStream atau membaca seluruh aliran data ke dalam array byte sekaligus. Pendekatan ini dapat mengakibatkan penurunan performa aplikasi dan potensi risiko Denial of Service (DoS), terutama untuk komponen sisi server. Sebagai gantinya, pertimbangkan untuk mengadopsi salah satu pendekatan berikut:

  • Salin aliran langsung ke file pada disk tanpa membacanya ke dalam memori. Perhatikan bahwa aplikasi yang Blazor menjalankan kode di server tidak dapat mengakses sistem file klien secara langsung.
  • Unggah file dari klien langsung ke layanan eksternal. Untuk informasi selengkapnya, lihat bagian Mengunggah file ke layanan eksternal.

Dalam contoh berikut, browserFile menerapkan IBrowserFile untuk mewakili file yang diunggah. Implementasi kerja untuk IBrowserFile ditampilkan dalam komponen unggahan file nanti di artikel ini.

Saat memanggil OpenReadStream, sebaiknya berikan ukuran file maksimum yang diizinkan dalam parameter maxAllowedSize pada batas ukuran file yang anda harapkan untuk menerima. Nilai defaultnya adalah 500 KB. Contoh artikel ini menggunakan variabel ukuran file maksimum yang diizinkan atau konstanta bernama maxFileSize tetapi biasanya tidak menampilkan pengaturan nilai tertentu.

Didukung: Pendekatan berikut direkomendasikan karena file Stream disediakan langsung kepada konsumen,FileStreamyang membuat file di jalur yang disediakan.

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

Didukung: Pendekatan berikut direkomendasikan untuk Microsoft Azure Blob Storage karena file Stream disediakan langsung ke UploadBlobAsync:

await blobContainerClient.UploadBlobAsync(
    trustedFileName, browserFile.OpenReadStream(maxFileSize));

✔️ Hanya direkomendasikan untuk file kecil: Pendekatan berikut ini hanya direkomendasikan untuk file kecil karena konten Stream file dibaca ke dalam MemoryStream dalam memori (memoryStream), yang menimbulkan penalti performa dan dos risiko. Untuk contoh yang menunjukkan teknik ini untuk menyimpan gambar mini dengan IBrowserFile ke database menggunakan Entity Framework Core (EF Core), lihat Simpan file kecil langsung ke database dengan bagian EF Core nanti di artikel ini.

using var memoryStream = new MemoryStream();
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
var smallFileByteArray = memoryStream.ToArray();

Tidak disarankan: Pendekatan berikut TIDAK disarankan karena konten file Stream dibaca ke dalam String memori (reader):

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

Tidak disarankan: Pendekatan berikut TIDAK disarankan untuk Microsoft Azure Blob Storage karena konten file Stream disalin ke dalam MemoryStream memori (memoryStream) sebelum memanggil UploadBlobAsync:

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

Komponen yang menerima file gambar dapat memanggil BrowserFileExtensions.RequestImageFileAsync metode kenyamanan pada file untuk mengubah ukuran data gambar dalam runtime JavaScript browser sebelum gambar dialirkan ke aplikasi. Penggunaan untuk panggilan RequestImageFileAsync paling tepat untuk aplikasi Blazor WebAssembly.

Pengguna kontainer Inversion of Control (IoC) Autofac

Jika Anda menggunakan Autofac Inversion of Control (IoC) container alih-alih kontainer injeksi dependensi ASP.NET Core bawaan, atur DisableImplicitFromServicesParameters ke true dalam opsi hub penangan sirkuit sisi server. Untuk informasi selengkapnya, lihat FileUpload: Tidak menerima data apa pun dalam waktu yang dialokasikan (dotnet/aspnetcore #38842).

Batas baca dan unggahan ukuran file

Untuk browser berbasis Chromium (misalnya, Google Chrome dan Microsoft Edge) yang menggunakan protokol HTTP/2, HTTPS, dan CORS, dukungan klien sisi Blazor menggunakan Streams API untuk memungkinkan pengunggahan file besar dengan streaming permintaan .

Tanpa browser Chromium, protokol HTTP/2, atau HTTPS, pada bagian klien Blazor membaca byte file ke dalam satu array buffer JavaScript saat memindahkan data dari JavaScript ke C#, yang terbatas pada 2 GB atau sesuai memori perangkat yang tersedia. Unggahan file besar mungkin gagal untuk unggahan sisi klien menggunakan komponen InputFile.

Sisi klien Blazor membaca byte dari file ke dalam sebuah buffer array JavaScript tunggal saat mentransfer data dari JavaScript ke C#, yang dibatasi hingga 2 GB atau sesuai dengan memori perangkat yang tersedia. Unggahan file besar mungkin gagal ketika diunggah dari sisi klien menggunakan komponen InputFile. Kami merekomendasikan mengadopsi streaming permintaan dengan ASP.NET Core 9.0 atau versi lebih baru.

Pertimbangan keamanan

Hindari IBrowserFile.Size untuk batas ukuran file

Hindari menggunakan IBrowserFile.Size untuk memberlakukan batas ukuran file. Alih-alih menggunakan ukuran file yang disediakan klien yang tidak aman, tentukan ukuran file maksimum secara eksplisit. Contoh berikut menggunakan ukuran file maksimum yang ditetapkan ke maxFileSize:

- var fileContent = new StreamContent(file.OpenReadStream(file.Size));
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

Keamanan nama file

Jangan pernah menggunakan nama file yang disediakan klien untuk menyimpan file ke penyimpanan fisik. Buat nama file yang aman untuk file menggunakan Path.GetRandomFileName() atau Path.GetTempFileName() untuk membuat jalur lengkap (termasuk nama file) untuk penyimpanan sementara.

Razor html otomatis mengodekan nilai properti untuk ditampilkan. Kode berikut aman digunakan:

@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}

Di luar Razor, selalu gunakan HtmlEncode untuk mengodekan nama file dengan aman dari permintaan pengguna.

Banyak penerapan harus menyertakan langkah untuk memastikan bahwa file tersebut ada; jika tidak demikian, file akan ditimpa oleh file lainnya dengan nama yang sama. Berikan logika tambahan untuk memenuhi spesifikasi aplikasi Anda.

Contoh

Contoh berikut menunjukkan beberapa unggahan file dalam komponen. InputFileChangeEventArgs.GetMultipleFiles memungkinkan membaca beberapa file. Tentukan jumlah maksimum file untuk mencegah pengguna berbahaya mengunggah jumlah file yang lebih besar dari yang diharapkan aplikasi. InputFileChangeEventArgs.File memungkinkan membaca file pertama dan satu-satunya jika unggahan file tidak mendukung beberapa file.

InputFileChangeEventArgs berada di Microsoft.AspNetCore.Components.Forms namespace, yang biasanya adalah salah satu namespace dalam file aplikasi _Imports.razor. Saat namespace ada dalam _Imports.razor file, namespace menyediakan akses anggota API ke komponen aplikasi.

Namespace dalam _Imports.razor file tidak diterapkan ke file C# (.cs). File C# memerlukan arahan eksplisit using di bagian atas file kelas:

using Microsoft.AspNetCore.Components.Forms;

Untuk menguji komponen pengunggahan file, Anda dapat membuat file pengujian dengan ukuran apa pun dengan PowerShell:

$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)

Pada perintah sebelumnya:

  • Tempat penampung {SIZE} adalah ukuran file dalam satuan byte (contohnya, 2097152 untuk file 2 MB).
  • Placeholder {PATH} adalah jalur dan nama file dengan ekstensi file (contohnya, D:/test_files/testfile2MB.txt).

Contoh unggahan file sisi server

Untuk menggunakan kode berikut, buat Development/unsafe_uploads folder di akar aplikasi yang berjalan di Development lingkungan.

Karena contoh menggunakan lingkungan aplikasi sebagai bagian dari jalur tempat file disimpan, folder tambahan diperlukan jika lingkungan lain digunakan dalam pengujian dan produksi. Misalnya, buat folder Staging/unsafe_uploads untuk lingkungan Staging. Buat folder Production/unsafe_uploads untuk lingkungan Production.

Peringatan

Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di 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 = [];
    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;
    }
}

Contoh unggahan file sisi klien

Contoh berikut memproses byte file dan tidak mengirim file ke tujuan di luar aplikasi. Untuk contoh Razor komponen yang mengirim file ke server atau layanan, lihat bagian berikut ini:

Komponen mengasumsikan bahwa mode render Interactive WebAssembly (InteractiveWebAssembly) diwarisi dari komponen induk atau diterapkan secara global ke aplikasi.

@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 = [];
    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 mengembalikan metadata yang diekspos oleh browser sebagai properti. Gunakan metadata ini untuk validasi awal.

Jangan pernah mempercayai nilai properti sebelumnya, terutama Name properti untuk ditampilkan di UI. Perlakukan semua data yang disediakan pengguna sebagai risiko keamanan yang signifikan terhadap aplikasi, server, dan jaringan. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.

Mengunggah file ke server dengan penyajian sisi server

Bagian ini berlaku untuk komponen Server Interaktif di Blazor Web Apps atau Blazor Server aplikasi.

Contoh berikut menunjukkan pengunggahan file dari aplikasi sisi server ke pengontrol API web backend di aplikasi terpisah, mungkin di server terpisah.

Dalam file aplikasi Program sisi server, tambahkan IHttpClientFactory dan layanan terkait yang memungkinkan aplikasi membuat HttpClient instans:

builder.Services.AddHttpClient();

Untuk informasi lebih lanjut, lihat Membuat permintaan HTTP menggunakan IHttpClientFactory di ASP.NET Core.

Untuk contoh di bagian ini:

  • API web berjalan di URL: https://localhost:5001
  • Aplikasi sisi server berjalan di URL: https://localhost:5003

Untuk pengujian, URL yang disebutkan sebelumnya dikonfigurasi dalam file proyek Properties/launchSettings.json.

Kelas berikut UploadResult mempertahankan hasil file yang diunggah. Ketika file gagal diunggah di server, kode kesalahan dikembalikan ErrorCode untuk ditampilkan kepada pengguna. Nama file aman dihasilkan di server untuk setiap file dan dikembalikan ke klien dalam StoredFileName untuk ditampilkan. File di-key antara klien dan server menggunakan nama file yang tidak aman/tidak tepercaya di 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; }
}

Praktik terbaik keamanan untuk aplikasi produksi adalah menghindari pengiriman pesan kesalahan ke klien yang mungkin mengungkapkan informasi sensitif tentang aplikasi, server, atau jaringan. Menyediakan pesan kesalahan terperinci dapat membantu pengguna berbahaya dalam merancang serangan pada aplikasi, server, atau jaringan. Contoh kode di bagian ini hanya mengirim kembali nomor kode kesalahan (int) untuk ditampilkan oleh sisi klien komponen jika terjadi kesalahan sisi server. Jika pengguna memerlukan bantuan dengan unggahan file, mereka memberikan kode kesalahan kepada personel dukungan untuk resolusi tiket dukungan tanpa pernah mengetahui penyebab pasti dari kesalahan tersebut.

Kelas berikut ini LazyBrowserFileStream mendefinisikan jenis aliran kustom yang memanggil secara lambat OpenReadStream sebelum byte pertama dari aliran diminta. Aliran tidak ditransmisikan dari browser ke server sampai pembacaan aliran dimulai di .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();
}

Komponen berikut FileUpload2 :

  • Mengizinkan pengguna mengunggah file dari klien.
  • Menampilkan nama file yang tidak tepercaya/tidak aman yang disediakan oleh klien di UI. Nama file yang tidak tepercaya/tidak aman secara otomatis dikodekan HTML oleh Razor untuk tampilan aman di UI.

Peringatan

Jangan percaya nama file yang disediakan oleh klien untuk:

  • Menyimpan file ke sistem atau layanan file.
  • Tampilkan di UI yang tidak mengodekan nama file secara otomatis atau melalui kode pengembang.

Untuk informasi selengkapnya tentang pertimbangan keamanan saat mengunggah file ke server, lihat Mengunggah file di 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 = [];
    private List<UploadResult> uploadResults = [];
    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; }
    }
}

Jika komponen membatasi pengunggahan file ke satu file pada satu waktu atau jika komponen hanya mengadopsi penyajian sisi klien (CSR, InteractiveWebAssembly), komponen dapat menghindari penggunaan LazyBrowserFileStream dan menggunakan Stream. Berikut ini menunjukkan perubahan untuk FileUpload2 komponen:

- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

LazyBrowserFileStream Hapus kelas (LazyBrowserFileStream.cs), karena tidak digunakan.

Jika komponen membatasi pengunggahan file ke satu file sekaligus, komponen dapat menghindari penggunaan LazyBrowserFileStream dan menggunakan Stream. Berikut ini menunjukkan perubahan untuk FileUpload2 komponen:

- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

LazyBrowserFileStream Hapus kelas (LazyBrowserFileStream.cs), karena tidak digunakan.

Pengontrol berikut dalam proyek API web menyimpan file yang diunggah dari klien.

Penting

Pengontrol di bagian ini dimaksudkan untuk digunakan dalam proyek API web terpisah dari Blazor aplikasi. API web harus mengurangi serangan Pemalsuan Permintaan Lintas Situs (XSRF/CSRF) jika pengguna pengunggah file diautentikasi.

Catatan

Mengikat nilai formulir dengan atribut [FromForm] tidak didukung secara native untuk Minimal APIs di ASP.NET Core di .NET 6. Oleh karena itu, contoh pengontrol berikut Filesave tidak dapat dikonversi untuk menggunakan API Minimal. Dukungan untuk mengikat dari nilai formulir dengan API Minimal tersedia di ASP.NET Core di .NET 7 atau yang lebih baru.

Untuk menggunakan kode berikut, buat Development/unsafe_uploads folder di akar proyek API web untuk aplikasi yang berjalan di Development lingkungan.

Karena contoh menggunakan lingkungan aplikasi sebagai bagian dari jalur tempat file disimpan, folder tambahan diperlukan jika lingkungan lain digunakan dalam pengujian dan produksi. Misalnya, buat folder Staging/unsafe_uploads pada lingkungan Staging. Buat folder Production/unsafe_uploads untuk lingkungan Production.

Peringatan

Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di 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);
    }
}

Dalam kode sebelumnya, GetRandomFileName dipanggil untuk menghasilkan nama file yang aman. Jangan pernah mempercayai nama file yang disediakan oleh browser, karena penyerang siber dapat memilih nama file yang ada yang menimpa file yang sudah ada atau mengirim jalur yang mencoba menulis di luar batas aplikasi.

Aplikasi server harus mendaftarkan layanan pengontrol dan titik akhir pengontrol peta. Untuk informasi selengkapnya, lihat Perutean ke tindakan kontroler di ASP.NET Core.

Mengunggah file ke server dengan penyajian sisi klien (CSR)

Bagian ini berlaku untuk komponen yang dirender sisi klien (CSR) dalam Blazor Web App atau aplikasi Blazor WebAssembly.

Contoh berikut menunjukkan pengunggahan file ke pengontrol API web backend di aplikasi terpisah, mungkin di server terpisah, dari komponen dalam Blazor Web App yang mengadopsi CSR atau komponen dalam Blazor WebAssembly aplikasi.

Contoh ini mengadopsi streaming permintaan untuk browser berbasis Chromium (misalnya, Google Chrome atau Microsoft Edge) dengan protokol HTTP/2 dan HTTPS. Jika streaming permintaan tidak dapat digunakan, Blazor secara bertahap beralih ke Fetch API tanpa menggunakan streaming permintaan. Untuk informasi selengkapnya, lihat bagian batas baca dan unggahan ukuran file .

Kelas berikut UploadResult mempertahankan hasil file yang diunggah. Ketika file gagal diunggah di server, kode kesalahan dikembalikan ErrorCode untuk ditampilkan kepada pengguna. Nama file aman dihasilkan di server untuk setiap file dan dikembalikan ke klien untuk ditampilkan dalam StoredFileName. File di-key antara klien dan server menggunakan nama file yang tidak aman/tidak tepercaya di 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; }
}

Catatan

Kelas UploadResult sebelumnya dapat dibagikan di antara proyek berbasis server dan klien. Saat klien dan proyek server berbagi kelas, tambahkan impor ke setiap file proyek _Imports.razor untuk proyek bersama. Contohnya:

@using BlazorSample.Shared

Komponen berikut FileUpload2 :

  • Mengizinkan pengguna mengunggah file dari klien.
  • Menampilkan nama file yang tidak tepercaya/tidak aman yang disediakan oleh klien di UI. Nama file yang tidak tepercaya/tidak aman secara otomatis dikodekan HTML oleh Razor untuk tampilan aman di UI.

Praktik terbaik keamanan untuk aplikasi produksi adalah menghindari pengiriman pesan kesalahan ke klien yang mungkin mengungkapkan informasi sensitif tentang aplikasi, server, atau jaringan. Menyediakan pesan kesalahan terperinci dapat membantu pengguna berbahaya dalam merancang serangan pada aplikasi, server, atau jaringan. Contoh kode di bagian ini hanya mengirim kembali nomor kode kesalahan (int) untuk ditampilkan oleh sisi klien komponen jika terjadi kesalahan sisi server. Jika pengguna memerlukan bantuan dengan unggahan file, mereka memberikan kode kesalahan kepada personel dukungan untuk penyelesaian tiket dukungan tanpa mengetahui penyebab pasti kesalahan.

Peringatan

Jangan percaya nama file yang disediakan oleh klien untuk:

  • Menyimpan file ke sistem atau layanan file.
  • Tampilkan di UI yang tidak mengodekan nama file secara otomatis atau melalui kode pengembang.

Untuk informasi selengkapnya tentang pertimbangan keamanan saat mengunggah file ke server, lihat Mengunggah file di ASP.NET Core.

Dalam proyek server Blazor Web App, tambahkan IHttpClientFactory dan layanan terkait dalam file Program proyek:

builder.Services.AddHttpClient();

Layanan HttpClient harus ditambahkan ke proyek server karena komponen sisi klien sudah di-prarender di server. Jika Anda menonaktifkan pra-rendering untuk komponen berikut, Anda tidak perlu menyediakan layanan HttpClient dalam proyek server dan tidak perlu menambahkan baris tersebut ke proyek server.

Untuk informasi selengkapnya tentang menambahkan HttpClient layanan ke aplikasi ASP.NET Core, lihat Membuat permintaan HTTP menggunakan IHttpClientFactory di ASP.NET Core.

Proyek klien (.Client) dari Blazor Web App juga harus mendaftarkan sebuah HttpClient untuk permintaan HTTP POST ke pengontrol API web di backend. Konfirmasi atau tambahkan yang berikut ini ke file proyek Program klien:

builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

Contoh sebelumnya mengatur alamat dasar dengan builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress), yang mendapatkan alamat dasar untuk aplikasi dan biasanya berasal dari <base> nilai tag href di halaman host. Jika Anda memanggil API web eksternal, atur URI ke alamat dasar API web.

Aplikasi Blazor WebAssembly mandiri yang mengunggah file ke API web di server terpisah menggunakan bernama HttpClient atau mengonfigurasi pendaftaran layanan HttpClient bawaan untuk mengarahkan ke titik akhir API web. Dalam contoh berikut di mana API web dihosting secara lokal di port 5001, alamat dasar https://localhost:5001:

builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri("https://localhost:5001") });

Dalam Blazor Web App, tambahkan namespace Microsoft.AspNetCore.Components.WebAssembly.Http ke direktif komponen:

@using Microsoft.AspNetCore.Components.WebAssembly.Http

FileUpload2.razor:

@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Net
@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 request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
            request.SetBrowserRequestStreamingEnabled(true);
            request.Content = content;

            var response = await Http.SendAsync(request);

            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
@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 = [];
    private List<UploadResult> uploadResults = [];
    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; }
    }
}

Pengontrol berikut dalam proyek sisi server menyimpan file yang diunggah dari klien.

Catatan

Mengikat nilai formulir dengan atribut [FromForm] tidak didukung secara bawaan untuk Minimal APIs di ASP.NET Core pada .NET 6. Oleh karena itu, contoh pengontrol berikut Filesave tidak dapat dikonversi untuk menggunakan API Minimal. Dukungan untuk mengikat dari nilai formulir dengan API Minimal tersedia di ASP.NET Core di .NET 7 atau yang lebih baru.

Untuk menggunakan kode berikut, buat Development/unsafe_uploads folder di akar proyek sisi server untuk aplikasi yang berjalan di Development lingkungan.

Karena contoh menggunakan lingkungan aplikasi sebagai bagian dari jalur tempat file disimpan, folder tambahan diperlukan jika lingkungan lain digunakan dalam pengujian dan produksi. Misalnya, buat folder Staging/unsafe_uploads untuk lingkungan Staging. Buat folder Production/unsafe_uploads untuk lingkungan Production.

Peringatan

Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.

Dalam contoh berikut untuk aplikasi Blazor WebAssembly yang dihosting atau di mana proyek bersama digunakan untuk menyediakan kelas UploadResult, tambahkan namespace proyek bersama:

using BlazorSample.Shared;

Sebaiknya gunakan namespace untuk pengontrol berikut (misalnya: namespace BlazorSample.Controllers).

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;

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

Dalam kode sebelumnya, GetRandomFileName dipanggil untuk menghasilkan nama file yang aman. Jangan pernah mempercayai nama file yang disediakan oleh browser, karena peretas dapat memilih nama file yang sudah ada untuk menimpa file tersebut atau mengirimkan jalur file yang mencoba menulis di luar aplikasi.

Aplikasi server harus mendaftarkan layanan pengontrol dan titik akhir pengontrol peta. Untuk informasi selengkapnya, lihat Perutean ke tindakan kontroler di ASP.NET Core. Kami merekomendasikan untuk menambahkan layanan pengontrol dengan AddControllersWithViews untuk secara otomatis mengatasi serangan Pemalsuan Permintaan Lintas Situs (XSRF/CSRF) pada pengguna yang diautentikasi. Jika Anda hanya menggunakan AddControllers, antiforgery tidak diaktifkan secara otomatis. Untuk informasi selengkapnya, lihat Perutean ke tindakan pengontrol di ASP.NET Core.

Konfigurasi Permintaan Lintas Asal (CORS) di server diperlukan untuk meminta streaming ketika server dihosting di asal yang berbeda, dan permintaan preflight selalu dibuat oleh klien. Dalam konfigurasi layanan file Program server (proyek server Blazor Web App atau API web server backend dari aplikasi Blazor WebAssembly), kebijakan CORS default berikut cocok untuk pengujian dengan contoh dalam artikel ini. Klien membuat permintaan lokal dari port 5003. Ubah nomor port agar sesuai dengan port aplikasi klien yang Anda gunakan:

Konfigurasikan Permintaan Lintas Asal (CORS) di server. Dalam konfigurasi layanan file Program server (proyek server Blazor Web App atau API web server backend dari aplikasi Blazor WebAssembly), kebijakan CORS default berikut cocok untuk pengujian dengan contoh dalam artikel ini. Klien membuat permintaan lokal dari port 5003. Ubah nomor port agar sesuai dengan port aplikasi klien yang Anda gunakan:

Konfigurasikan Permintaan Lintas Asal (CORS) di server. Dalam konfigurasi layanan pada file Program API web server backend, kebijakan CORS default berikut cocok untuk pengujian dengan contoh dalam artikel ini. Klien membuat permintaan lokal dari port 5003. Ubah nomor port agar sesuai dengan port aplikasi klien yang Anda gunakan:

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("https://localhost:5003")
                  .AllowAnyMethod()
                  .AllowAnyHeader();
        });
});

Setelah memanggil UseHttpsRedirection dalam file Program, panggil UseCors untuk menambahkan middleware CORS:

app.UseCors();

Untuk informasi lebih lanjut, silakan lihat Mengaktifkan Permintaan Lintas-Asal (CORS) di ASP.NET Core.

Konfigurasikan ukuran isi permintaan maksimum server dan batas panjang isi multipisah jika batasan membatasi ukuran unggahan.

Untuk server Kestrel, atur MaxRequestBodySize (default: 30.000.000 byte) dan FormOptions.MultipartBodyLengthLimit (default: 134.217.728 byte). Atur variabel maxFileSize dalam komponen dan pengontrol ke nilai yang sama.

Dalam konfigurasi Kestrel file Program berikut (proyek server Blazor Web App atau API web server backend dari aplikasi Blazor WebAssembly), tempat penampung {LIMIT} adalah batas dalam byte:

using Microsoft.AspNetCore.Http.Features;

...

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxRequestBodySize = {LIMIT};
});

builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = {LIMIT};
});

Membatalkan unggahan file

Komponen pengunggahan file dapat mendeteksi kapan pengguna telah membatalkan unggahan dengan menggunakan CancellationToken saat memanggil ke IBrowserFile.OpenReadStream atau StreamReader.ReadAsync.

Buat CancellationTokenSource untuk komponen InputFile. Di awal OnInputFileChange metode, periksa apakah unggahan sebelumnya sedang berlangsung.

Jika unggahan file sedang berlangsung:

Unggah file di sisi server dengan status kemajuan

Contoh berikut menunjukkan cara mengunggah file di aplikasi sisi server dengan kemajuan pengunggahan yang ditampilkan kepada pengguna.

Untuk menggunakan contoh berikut dalam aplikasi pengujian:

  • Buat folder untuk menyimpan file yang Development diunggah untuk lingkungan: Development/unsafe_uploads.
  • Konfigurasikan ukuran file maksimum (maxFileSize, 15 KB dalam contoh berikut) dan jumlah maksimum file yang diizinkan (maxAllowedFiles, 3 dalam contoh berikut).
  • Atur buffer ke nilai yang berbeda (10 KB dalam contoh berikut), jika diinginkan, untuk peningkatan granularitas dalam pelaporan yang sedang berlangsung. Kami tidak merekomendasikan penggunaan buffer yang lebih besar dari 30 KB karena masalah performa dan keamanan.

Peringatan

Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di 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 = [];
    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;
    }
}

Untuk informasi selengkapnya, lihat sumber daya API berikut ini:

  • FileStream: Menyediakan Stream untuk file, mendukung operasi baca dan tulis yang sinkron dan asinkron.
  • FileStream.ReadAsync: Komponen sebelumnya FileUpload3 membaca aliran secara asinkron dengan ReadAsync. Membaca aliran secara sinkron dengan Read tidak didukung dalam komponen Razor.

Aliran berkas

Dengan interaktivitas server, data file dialirkan melalui SignalR koneksi ke dalam kode .NET di server saat file dibaca.

RemoteBrowserFileStreamOptions memungkinkan konfigurasi karakteristik pengunggahan file.

Untuk komponen yang dirender melalui WebAssembly, data dari file dialirkan langsung ke dalam kode .NET di dalam browser.

Unggah pratinjau gambar

Untuk pratinjau gambar saat mengunggah, mulailah dengan menambahkan sebuah komponen InputFile dengan referensi komponen dan penangan OnChange.

<InputFile @ref="inputFile" OnChange="ShowPreview" />

Tambahkan elemen gambar dengan elemen referensi , yang berfungsi sebagai tempat penampung untuk pratinjau gambar:

<img @ref="previewImageElem" />

Tambahkan referensi terkait:

@code {
    private InputFile? inputFile;
    private ElementReference previewImageElem;
}

Di JavaScript, tambahkan fungsi yang disebut dengan HTML input dan img elemen yang melakukan hal berikut:

  • Mengekstrak file terpilih.
  • Membuat URL objek dengan createObjectURL.
  • Mengatur pendengar acara untuk mencabut URL objek dengan revokeObjectURL setelah gambar dimuat, agar memori tidak bocor.
  • img Mengatur sumber elemen untuk menampilkan gambar.
window.previewImage = (inputElem, imgElem) => {
  const url = URL.createObjectURL(inputElem.files[0]);
  imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
  imgElem.src = url;
}

Terakhir, gunakan IJSRuntime yang disuntikkan untuk menambahkan handler OnChange yang memanggil fungsi JavaScript.

@inject IJSRuntime JS

...

@code {
    ...

    private async Task ShowPreview() => await JS.InvokeVoidAsync(
        "previewImage", inputFile!.Element, previewImageElem);
}

Contoh sebelumnya adalah untuk mengunggah satu gambar. Pendekatan dapat diperluas untuk mendukung multiple gambar.

Komponen berikut FileUpload4 menunjukkan contoh lengkapnya.

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

Menyimpan file kecil langsung ke database dengan EF Core

Banyak aplikasi ASP.NET Core menggunakan Entity Framework Core (EF Core) untuk mengelola operasi database. Menyimpan gambar mini dan avatar langsung ke database adalah persyaratan umum. Bagian ini menunjukkan pendekatan umum yang dapat ditingkatkan lebih lanjut untuk aplikasi produksi.

Pola berikut:

  • Didasarkan pada sebuah aplikasi tutorial database film Blazor.
  • Dapat ditingkatkan dengan kode tambahan untuk umpan balik validasi ukuran file dan jenis konten .
  • Menanggung penalti kinerja dan risiko DoS. Pertimbangkan risiko dengan hati-hati saat membaca file apa pun ke dalam memori dan pertimbangkan pendekatan alternatif, terutama untuk file yang lebih besar. Pendekatan alternatif termasuk menyimpan file langsung ke disk atau layanan pihak ketiga untuk pemeriksaan antivirus/antimalware, pemrosesan lebih lanjut, dan melayani kepada klien.

Agar contoh berikut berfungsi di Blazor Web App (ASP.NET Core 8.0 atau yang lebih baru), komponen harus mengadopsi mode render interaktif (misalnya, @rendermode InteractiveServer) untuk memanggil HandleSelectedThumbnail pada perubahan file komponen InputFile ( parameter/peristiwaOnChange). Blazor Server komponen aplikasi selalu interaktif dan tidak memerlukan mode render.

Dalam contoh berikut, thumbnail kecil (<= 100 KB) dalam IBrowserFile disimpan ke database dengan EF Core. Jika file tidak dipilih oleh pengguna untuk komponen InputFile, gambar mini default disimpan ke database.

Gambar mini default (default-thumbnail.jpg) berada di akar proyek dengan pengaturan Salin ke Direktori OutputSalin jika lebih baru:

Gambar mini generik default

Model Movie (Movie.cs) memiliki properti (Thumbnail) untuk menyimpan data gambar mini:

[Column(TypeName = "varbinary(MAX)")]
public byte[]? Thumbnail { get; set; }

Data gambar disimpan dalam bentuk byte dalam database sebagai varbinary(MAX). Aplikasi base-64 mengodekan byte untuk ditampilkan karena data yang dikodekan base-64 kira-kira merupakan yang ketiga lebih besar dari byte mentah gambar, sehingga data gambar base-64 memerlukan penyimpanan database tambahan dan mengurangi performa operasi baca/tulis database.

Komponen yang menampilkan gambar mini meneruskan data gambar ke atribut src tag img sebagai data JPEG yang dikodekan dalam base-64.

<img src="data:image/jpeg;base64,@Convert.ToBase64String(movie.Thumbnail)" 
    alt="User thumbnail" />

Dalam komponen Create berikut, unggahan gambar diproses. Anda dapat meningkatkan contoh lebih lanjut dengan validasi kustom untuk jenis dan ukuran file menggunakan pendekatan dalam validasi formulir ASP.NET Core Blazor. Untuk melihat komponen Create secara lengkap tanpa kode unggahan gambar mini pada contoh berikut, lihat aplikasi contoh BlazorWebAppMovies di repositori sampel Blazor di GitHub.

Components/Pages/MoviePages/Create.razor:

@page "/movies/create"
@rendermode InteractiveServer
@using Microsoft.EntityFrameworkCore
@using BlazorWebAppMovies.Models
@inject IDbContextFactory<BlazorWebAppMovies.Data.BlazorWebAppMoviesContext> DbFactory
@inject NavigationManager NavigationManager

...

<div class="row">
    <div class="col-md-4">
        <EditForm method="post" Model="Movie" OnValidSubmit="AddMovie" 
            FormName="create" Enhance>
            <DataAnnotationsValidator />
            <ValidationSummary class="text-danger" role="alert"/>

            ...

            <div class="mb-3">
                <label for="thumbnail" class="form-label">Thumbnail:</label>
                <InputFile id="thumbnail" OnChange="HandleSelectedThumbnail" 
                    class="form-control" />
            </div>
            <button type="submit" class="btn btn-primary">Create</button>
        </EditForm>
    </div>
</div>

...

@code {
    private const long maxFileSize = 102400;
    private IBrowserFile? browserFile;

    [SupplyParameterFromForm]
    private Movie Movie { get; set; } = new();

    private void HandleSelectedThumbnail(InputFileChangeEventArgs e)
    {
        browserFile = e.File;
    }

    private async Task AddMovie()
    {
        using var context = DbFactory.CreateDbContext();

        if (browserFile?.Size > 0 && browserFile?.Size <= maxFileSize)
        {
            using var memoryStream = new MemoryStream();
            await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);

            Movie.Thumbnail = memoryStream.ToArray();
        }
        else
        {
            Movie.Thumbnail = File.ReadAllBytes(
                $"{AppDomain.CurrentDomain.BaseDirectory}default_thumbnail.jpg");
        }

        context.Movie.Add(Movie);
        await context.SaveChangesAsync();
        NavigationManager.NavigateTo("/movies");
    }
}

Pendekatan yang sama akan diadopsi dalam komponen Edit dengan mode render interaktif jika pengguna diizinkan untuk mengedit gambar mini film.

Mengunggah file ke layanan eksternal

Alih-alih aplikasi yang menangani byte pengunggahan file dan server aplikasi yang menerima file yang diunggah, klien dapat langsung mengunggah file ke layanan eksternal. Aplikasi ini dapat memproses file dengan aman dari layanan eksternal sesuai permintaan. Pendekatan ini memperkuat aplikasi dan servernya terhadap serangan berbahaya dan potensi masalah performa.

Pertimbangkan pendekatan yang menggunakan Azure Files, Azure Blob Storage, atau layanan pihak ketiga dengan manfaat potensial berikut:

Untuk informasi selengkapnya tentang Azure Blob Storage dan Azure Files, lihat dokumentasi Azure Storage.

Batas ukuran pesan pada sisi SignalR server

Unggahan file mungkin gagal bahkan sebelum dimulai, saat Blazor mengambil data tentang file yang melebihi ukuran pesan maksimum SignalR .

SignalR menentukan batas ukuran pesan yang berlaku untuk setiap pesan Blazor yang diterima, dan InputFile komponen mengalirkan file ke server dalam pesan yang mematuhi batas yang dikonfigurasi. Namun, pesan pertama, yang menunjukkan kumpulan file yang akan diunggah, dikirim sebagai pesan tunggal yang unik. Ukuran pesan pertama mungkin melebihi SignalR batas ukuran pesan. Masalah ini tidak terkait dengan ukuran file, ini terkait dengan jumlah file.

Kesalahan yang dicatat mirip dengan yang berikut ini:

Kesalahan: Koneksi terputus dengan kesalahan 'Kesalahan: Server mengembalikan kesalahan saat ditutup: Koneksi ditutup dengan kesalahan.'. e.log @ blazor.server.js:1

Saat mengunggah file, mencapai batas ukuran pesan pada pesan pertama jarang terjadi. Jika batas tercapai, aplikasi dapat mengonfigurasi HubOptions.MaximumReceiveMessageSize dengan nilai yang lebih besar.

Untuk informasi selengkapnya tentang SignalR konfigurasi dan cara mengatur MaximumReceiveMessageSize, lihat panduan ASP.NET Core BlazorSignalR.

Pemanggilan paralel maksimum per pengaturan hub klien

Blazor bergantung pada MaximumParallelInvocationsPerClient yang disetel ke 1, yang merupakan nilai default.

Meningkatkan nilai mengarah pada kemungkinan tinggi bahwa operasi CopyTo akan melempar System.InvalidOperationException: 'Reading is not allowed after reader was completed.'. Untuk informasi selengkapnya, lihat MaximumParallelInvocationsPerClient > 1 memutus unggahan file dalam Blazor Server mode (dotnet/aspnetcore #53951).

Pecahkan masalah

Baris yang memanggil IBrowserFile.OpenReadStream melempar System.TimeoutException:

System.TimeoutException: Did not receive any data in the allotted time.

Kemungkinan penyebabnya:

Sumber Daya Tambahan: