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 maxAllowedSize
OpenReadStream 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:
- Panggil Cancel pada unggahan sebelumnya.
- Buat baru CancellationTokenSource untuk unggahan berikutnya dan teruskan CancellationTokenSource.Token ke OpenReadStream atau ReadAsync.
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
<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:
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:
- Unggah file dari klien langsung ke layanan eksternal dengan pustaka klien JavaScript atau REST API. Misalnya, Azure menawarkan pustaka dan API klien berikut:
- Otorisasi unggahan pengguna dengan token tanda tangan akses bersama (SAS) yang didelegasikan pengguna yang dihasilkan oleh aplikasi (sisi server) untuk setiap unggahan file klien. Misalnya, Azure menawarkan fitur SAS berikut:
- Sediakan redundansi otomatis dan pencadangan untuk berbagi file.
- Batasi unggahan dengan kuota. Perhatikan bahwa kuota Azure Blob Storage diatur pada tingkat akun, bukan tingkat kontainer. Namun, kuota Azure Files berada di tingkat berbagi file dan mungkin memberikan kontrol yang lebih baik atas batas unggahan. Untuk informasi selengkapnya, lihat dokumen Azure yang ditautkan sebelumnya dalam daftar ini.
- File aman dengan enkripsi sisi server (SSE).
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:
Menggunakan kontainer Autofac Inversion of Control (IoC) alih-alih kontainer injeksi dependensi ASP.NET Core bawaan pada versi ASP.NET Core sebelum 9.0. Untuk mengatasi masalah ini, atur DisableImplicitFromServicesParameters ke
true
di opsi hub handler sirkuit sisi server. Untuk informasi selengkapnya, lihat FileUpload: Tidak menerima data apa pun dalam waktu yang dialokasikan (dotnet/aspnetcore
#38842).Tidak membaca stream sampai selesai. Ini bukan masalah kerangka kerja. Tangkap pengecualian dan selidiki lebih lanjut di lingkungan/jaringan lokal Anda.
- Menggunakan rendering sisi server dan memanggil OpenReadStream pada beberapa file sebelum membacanya hingga selesai. Untuk mengatasi masalah ini, gunakan kelas
LazyBrowserFileStream
dan pendekatan yang dijelaskan di bagian Unggah file ke server dengan rendering sisi server di artikel ini.
Sumber Daya Tambahan:
ASP.NET Core