Udostępnij za pośrednictwem


przekazywanie plików ASP.NET Core Blazor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

W tym artykule wyjaśniono, jak przekazywać pliki za Blazor pomocą InputFile składnika.

Operacje przekazywania plików

Ostrzeżenie

Zawsze przestrzegaj najlepszych rozwiązań w zakresie zabezpieczeń, gdy zezwalają użytkownikom na przekazywanie plików. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.

InputFile Użyj składnika, aby odczytać dane pliku przeglądarki do kodu platformy .NET. Składnik InputFile renderuje element HTML <input> typu file dla przekazywania pojedynczych plików. multiple Dodaj atrybut, aby zezwolić użytkownikowi na przekazywanie wielu plików jednocześnie.

Wybór pliku nie jest skumulowany w przypadku używania składnika lub jego bazowego InputFilekodu HTML <input type="file">, więc nie można dodawać plików do istniejącego zaznaczenia pliku. Składnik zawsze zastępuje początkowy wybór pliku użytkownika, więc odwołania do plików z poprzednich wyborów nie są dostępne.

Poniższy InputFile składnik wykonuje metodę LoadFiles , gdy OnChange wystąpi zdarzenie (change). Element InputFileChangeEventArgs zapewnia dostęp do wybranej listy plików i szczegółów dotyczących każdego pliku:

<InputFile OnChange="LoadFiles" multiple />

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

Renderowany kod HTML:

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

Uwaga

W poprzednim przykładzie <input> atrybut elementu _bl_2 jest używany do Blazorwewnętrznego przetwarzania.

Aby odczytać dane z wybranego przez użytkownika pliku z wartością Stream reprezentującą bajty pliku, wywołaj IBrowserFile.OpenReadStream plik i odczytaj z zwróconego strumienia. Aby uzyskać więcej informacji, zobacz sekcję Strumienie plików.

OpenReadStream wymusza maksymalny rozmiar w bajtach jego Streamwartości . Odczytywanie jednego pliku lub wielu plików większych niż 500 KB powoduje wyjątek. Ten limit uniemożliwia deweloperom przypadkowe odczytywanie dużych plików do pamięci. Parametr maxAllowedSize parametru można użyć do określenia większego rozmiaru OpenReadStream , jeśli jest to wymagane.

Poza przetwarzaniem małego pliku unikaj odczytywania przychodzącego strumienia plików bezpośrednio do pamięci jednocześnie. Na przykład nie kopiuj wszystkich bajtów pliku do MemoryStream ani nie odczytaj całego strumienia do tablicy bajtów jednocześnie. Te podejścia mogą spowodować obniżenie wydajności aplikacji i potencjalne ryzyko odmowy usługi (DoS ), szczególnie w przypadku składników po stronie serwera. Zamiast tego należy rozważyć przyjęcie jednego z następujących podejść:

  • Skopiuj strumień bezpośrednio do pliku na dysku bez odczytywania go do pamięci. Należy pamiętać, że Blazor aplikacje wykonujące kod na serwerze nie mogą bezpośrednio uzyskać dostępu do systemu plików klienta.
  • Przekazywanie plików z klienta bezpośrednio do usługi zewnętrznej. Aby uzyskać więcej informacji, zobacz sekcję Przekazywanie plików do usługi zewnętrznej.

W poniższych przykładach browserFile implementuje IBrowserFile do reprezentowania przekazanego pliku. Działające implementacje programu IBrowserFile są wyświetlane w składnikach przekazywania plików w dalszej części tego artykułu.

Podczas wywoływania OpenReadStream zalecamy przekazanie maksymalnego dozwolonego rozmiaru pliku w parametrze maxAllowedSize, zgodnie z limitem rozmiarów plików, które oczekujesz otrzymać. Wartość domyślna to 500 KB. W przykładach tego artykułu użyto zmiennej lub stałej o nazwie maxFileSize do określenia maksymalnego dozwolonego rozmiaru pliku, ale zwykle nie pokazują one ustawienia konkretnej wartości.

Obsługiwane: zalecane jest następujące podejście, ponieważ plik Stream jest udostępniany bezpośrednio użytkownikowi, czyli plik, który FileStream tworzy plik w podanej ścieżce:

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

Obsługiwane: w przypadku usługi Microsoft Azure Blob Storage zalecane jest następujące podejście, ponieważ plik Stream jest dostarczany bezpośrednio do UploadBlobAsyncusługi :

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

Zalecane tylko w przypadku małych plików: następujące podejście jest zalecane tylko w przypadku małych plików , ponieważ zawartość pliku Stream jest odczytywana w MemoryStream pamięci (memoryStream), co wiąże się z karą za wydajność i ryzykiem systemu DoS . Przykład, który demonstruje tę technikę zapisywania obrazu miniatury w bazie danych z użyciem IBrowserFile, przy pomocy narzędzia Entity Framework Core (EF Core), znajdziesz w sekcji Zapisywanie małych plików bezpośrednio do bazy danych z EF Core dalej w artykule.

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

Niezalecane: Następujące podejście nie jest zalecane , ponieważ zawartość pliku Stream jest odczytywana w String pamięci (reader):

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

Niezalecane: następujące podejście nie jest zalecane w przypadku usługi Microsoft Azure Blob Storage , ponieważ zawartość pliku Stream jest kopiowana do MemoryStream pamięci (memoryStream) przed wywołaniem metody UploadBlobAsync:

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

Składnik odbierający plik obrazu może wywołać BrowserFileExtensions.RequestImageFileAsync metodę wygody w pliku, aby zmienić rozmiar danych obrazu w środowisku uruchomieniowym JavaScript przeglądarki przed przesyłaniem strumieniowym obrazu do aplikacji. Przypadki użycia wywołania RequestImageFileAsync są najbardziej odpowiednie dla Blazor WebAssembly aplikacji.

Autofac Inversion of Control (IoC) container users (Autofac Inversion of Control) users (Autofac Inversion of Control ( IoC) container users (Autofac In

Jeśli używasz kontenera Autofac Inversion of Control (IoC) zamiast wbudowanego kontenera iniekcji zależności ASP.NET Core, ustaw wartość DisableImplicitFromServicesParameters na true w opcjach centrum obsługi obwodu po stronie serwera. Aby uzyskać więcej informacji, zobacz FileUpload: Nie odebrano żadnych danych w czasie przydzielonym (dotnet/aspnetcore #38842).

Limity odczytu i przekazywania rozmiaru pliku

W przypadku przeglądarek opartych na chromium (na przykład Google Chrome i Microsoft Edge) przy użyciu protokołu HTTP/2, protokołu HTTPS i mechanizmu CORS po stronie Blazor klienta obsługiwane jest używanie interfejsu API strumieni w celu zezwolenia na przekazywanie dużych plików przy użyciu przesyłania strumieniowego żądań.

Bez przeglądarki Chromium, protokołu HTTP/2 lub HTTPS, Blazor po stronie klienta odczytuje bajty pliku do pojedynczego bufora tablicy JavaScript podczas przekształcania danych z JavaScript do C#, co jest ograniczone do 2 GB lub dostępnej pamięci urządzenia. Przesyłanie dużych plików może zakończyć się niepowodzeniem w przypadku przesyłania po stronie klienta przy użyciu InputFile składnika.

Aplikacja po stronie Blazor klienta odczytuje bajty pliku do pojedynczego bufora tablicy JavaScript, gdy przenosi dane z języka JavaScript do języka C#. Proces ten jest ograniczony do 2 GB lub do ilości dostępnej pamięci urządzenia. Przesyłanie dużych plików może zakończyć się niepowodzeniem w przypadku przesyłania po stronie klienta przy użyciu InputFile składnika. Zalecamy wdrożenie przesyłania strumieniowego żądań przy użyciu platformy .NET 9 lub nowszej.

Zagadnienia dotyczące zabezpieczeń

Unikaj IBrowserFile.Size limitów rozmiaru pliku

Unikaj używania IBrowserFile.Size polecenia , aby narzucić limit rozmiaru pliku. Zamiast używać niebezpiecznego rozmiaru pliku dostarczonego przez klienta, jawnie określ maksymalny rozmiar pliku. W poniższym przykładzie użyto maksymalnego rozmiaru pliku przypisanego do maxFileSize:

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

Zabezpieczenia nazw plików

Nigdy nie używaj nazwy pliku dostarczonego przez klienta do zapisywania pliku w magazynie fizycznym. Utwórz bezpieczną nazwę pliku przy użyciu Path.GetRandomFileName() lub Path.GetTempFileName() aby utworzyć pełną ścieżkę (w tym nazwę pliku) do tymczasowego przechowywania.

Razor automatycznie koduje wartości właściwości html do wyświetlania. Poniższy kod jest bezpieczny do użycia:

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

Poza Razor, zawsze używaj funkcji HtmlEncode, aby bezpiecznie kodować nazwy plików z żądania użytkownika.

Wiele implementacji musi zawierać sprawdzenie, czy plik istnieje; w przeciwnym razie plik jest zastępowany przez plik o tej samej nazwie. Podaj dodatkową logikę, aby spełnić specyfikacje aplikacji.

Przykłady

W poniższych przykładach pokazano wiele przekazywania plików w składniku. InputFileChangeEventArgs.GetMultipleFiles umożliwia odczytywanie wielu plików. Określ maksymalną liczbę plików, aby uniemożliwić złośliwemu użytkownikowi przekazywanie większej liczby plików niż oczekiwana przez aplikację. InputFileChangeEventArgs.File umożliwia odczytywanie pierwszego pliku i tylko wtedy, gdy przekazywanie pliku nie obsługuje wielu plików.

InputFileChangeEventArgs znajduje się w Microsoft.AspNetCore.Components.Forms przestrzeni nazw, która jest zazwyczaj jedną z przestrzeni nazw w pliku aplikacji _Imports.razor . Gdy przestrzeń nazw znajduje się w _Imports.razor pliku, zapewnia dostęp do składowych interfejsu API do składników aplikacji.

Przestrzenie nazw w _Imports.razor pliku nie są stosowane do plików C# (.cs). Pliki języka C# wymagają jawnej using dyrektywy w górnej części pliku klasy:

using Microsoft.AspNetCore.Components.Forms;

W przypadku testowania składników przekazywania plików można utworzyć pliki testowe o dowolnym rozmiarze za pomocą programu PowerShell:

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

W powyższym poleceniu:

  • Symbol {SIZE} zastępczy to rozmiar pliku w bajtach (na przykład 2097152 dla pliku o rozmiarze 2 MB).
  • Symbol {PATH} zastępczy to ścieżka i plik z rozszerzeniem pliku (na przykład D:/test_files/testfile2MB.txt).

Przykład przekazywania pliku po stronie serwera

Aby użyć następującego kodu, utwórz Development/unsafe_uploads folder w katalogu głównym aplikacji działającej Development w środowisku.

Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads folder dla Staging środowiska. Production/unsafe_uploads Utwórz folder dla Production środowiska.

Ostrzeżenie

Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie 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;
    }
}

Przykład przekazywania pliku po stronie klienta

Poniższy przykład przetwarza bajty plików i nie wysyła plików do miejsca docelowego poza aplikacją. Razor Przykładowy składnik, który wysyła plik do serwera lub usługi, zobacz następujące sekcje:

Składnik zakłada, że tryb renderowania interactive WebAssembly (InteractiveWebAssembly) jest dziedziczony z składnika nadrzędnego lub stosowany globalnie do aplikacji.

@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 zwraca metadane uwidocznione przez przeglądarkę jako właściwości. Użyj tych metadanych do wstępnej weryfikacji.

Nigdy nie ufaj wartościom poprzednich właściwości, zwłaszcza Name właściwości do wyświetlania w interfejsie użytkownika. Traktuj wszystkie dane dostarczone przez użytkownika jako istotne zagrożenie bezpieczeństwa aplikacji, serwera i sieci. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.

Przekazywanie plików na serwer za pomocą renderowania po stronie serwera

Ta sekcja dotyczy składników interactive server w Blazor Web Appsystemach lub Blazor Server aplikacjach.

W poniższym przykładzie pokazano przekazywanie plików z aplikacji po stronie serwera do kontrolera internetowego interfejsu API zaplecza w oddzielnej aplikacji, prawdopodobnie na osobnym serwerze.

W pliku aplikacji Program po stronie serwera dodaj IHttpClientFactory i powiązane usługi, które umożliwiają aplikacji tworzenie HttpClient wystąpień:

builder.Services.AddHttpClient();

Aby uzyskać więcej informacji, zobacz Tworzenie żądań HTTP za pomocą interfejsu IHttpClientFactory na platformie ASP.NET Core.

Przykłady w tej sekcji:

  • Internetowy interfejs API jest uruchamiany pod adresem URL: https://localhost:5001
  • Aplikacja po stronie serwera działa pod adresem URL: https://localhost:5003

Na potrzeby testowania poprzednie adresy URL są konfigurowane w plikach projektów Properties/launchSettings.json .

Poniższa UploadResult klasa zachowuje wynik przekazanego pliku. Gdy przekazywanie pliku na serwerze nie powiedzie się, zostanie zwrócony ErrorCode kod błędu w celu wyświetlenia użytkownikowi. Bezpieczna nazwa pliku jest generowana na serwerze dla każdego pliku i zwracana do klienta w StoredFileName programie w celu wyświetlenia. Pliki są kluczami między klientem a serwerem przy użyciu niebezpiecznej/niezaufanej nazwy pliku w programie 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; }
}

Najlepszym rozwiązaniem w zakresie zabezpieczeń dla aplikacji produkcyjnych jest unikanie wysyłania komunikatów o błędach do klientów, którzy mogą ujawniać poufne informacje o aplikacji, serwerze lub sieci. Udostępnianie szczegółowych komunikatów o błędach może pomóc złośliwego użytkownika w opracowywaniu ataków na aplikację, serwer lub sieć. Przykładowy kod w tej sekcji wysyła tylko numer kodu błędu (int) do wyświetlenia po stronie klienta składnika, jeśli wystąpi błąd po stronie serwera. Jeśli użytkownik wymaga pomocy przy przekazaniu pliku, podaj kod błędu pomocy technicznej dla personelu pomocy technicznej w celu rozwiązania biletu pomocy technicznej bez znajomości dokładnej przyczyny błędu.

Poniższa LazyBrowserFileStream klasa definiuje niestandardowy typ strumienia, który leniwie wywołuje OpenReadStream tuż przed żądaniem pierwszych bajtów strumienia. Strumień nie jest przesyłany z przeglądarki do serwera do momentu rozpoczęcia odczytu strumienia na platformie .NET.

LazyBrowserFileStream.cs:

using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize) 
    : Stream
{
    private readonly IBrowserFile file = file;
    private readonly int maxAllowedSize = maxAllowedSize;
    private Stream? underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public override void Flush() => underlyingStream?.Flush();

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen() => 
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream : Stream
{
    private readonly IBrowserFile file;
    private readonly int maxAllowedSize;
    private Stream? underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
    {
        this.file = file;
        this.maxAllowedSize = maxAllowedSize;
    }

    public override void Flush()
    {
        underlyingStream?.Flush();
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen()
    {
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);
    }

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream : Stream
{
    private readonly IBrowserFile file;
    private readonly int maxAllowedSize;
    private Stream? underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
    {
        this.file = file;
        this.maxAllowedSize = maxAllowedSize;
    }

    public override void Flush()
    {
        underlyingStream?.Flush();
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen()
    {
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);
    }

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream : Stream
{
    private readonly IBrowserFile file;
    private readonly int maxAllowedSize;
    private Stream underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
    {
        this.file = file;
        this.maxAllowedSize = maxAllowedSize;
    }

    public override void Flush()
    {
        underlyingStream?.Flush();
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen()
    {
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);
    }

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}

FileUpload2 Następujący składnik:

  • Zezwala użytkownikom na przekazywanie plików z klienta.
  • Wyświetla niezaufaną/niebezpieczną nazwę pliku podaną przez klienta w interfejsie użytkownika. Niezaufana/niebezpieczna nazwa pliku jest automatycznie zakodowana w Razor formacie HTML w celu bezpiecznego wyświetlania w interfejsie użytkownika.

Ostrzeżenie

Nie ufaj nazwam plików dostarczonym przez klientów dla:

  • Zapisywanie pliku w systemie plików lub usłudze.
  • Wyświetlaj w interfejsach użytkownika, które nie kodują automatycznie nazw plików ani za pośrednictwem kodu dewelopera.

Aby uzyskać więcej informacji na temat zagadnień dotyczących zabezpieczeń podczas przekazywania plików na serwer, zobacz Przekazywanie plików w programie 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();

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

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

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

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

Jeśli składnik ogranicza przesyłanie plików do pojedynczego pliku w danym momencie lub jeśli przyjmuje renderowanie po stronie klienta (CSR, InteractiveWebAssembly), może uniknąć użycia LazyBrowserFileStream i użyć Stream. Poniżej przedstawiono zmiany składnika FileUpload2 :

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

Usuń klasę LazyBrowserFileStream (LazyBrowserFileStream.cs), ponieważ nie jest używana.

Jeśli składnik ogranicza przekazywanie plików do pojedynczego pliku w danym momencie, składnik może uniknąć użycia LazyBrowserFileStream elementu i użyć elementu Stream. Poniżej przedstawiono zmiany składnika FileUpload2 :

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

Usuń klasę LazyBrowserFileStream (LazyBrowserFileStream.cs), ponieważ nie jest używana.

Następujący kontroler w projekcie internetowego interfejsu API zapisuje przekazane pliki z klienta.

Ważne

Kontroler w tej sekcji jest przeznaczony do użycia w osobnym projekcie internetowego interfejsu Blazor API z aplikacji. Internetowy interfejs API powinien ograniczyć ryzyko fałszowania żądań między witrynami (XSRF/CSRF) w przypadku uwierzytelnienia użytkowników przekazywania plików.

Uwaga

Powiązanie wartości formularza z atrybutem[FromForm]nie jest natywnie obsługiwane dla minimalnych interfejsów API w programie ASP.NET Core na platformie .NET 6. W związku z tym nie można przekonwertować następującego Filesave przykładu kontrolera w celu użycia interfejsów API minimalnych. Obsługa powiązań z wartości formularza z minimalnymi interfejsami API jest dostępna w programie ASP.NET Core na platformie .NET 7 lub nowszym.

Aby użyć następującego kodu, utwórz Development/unsafe_uploads folder w katalogu głównym projektu internetowego interfejsu API dla aplikacji działającej Development w środowisku.

Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads folder dla Staging środowiska. Production/unsafe_uploads Utwórz folder dla Production środowiska.

Ostrzeżenie

Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie 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);
    }
}

W poprzednim kodzie GetRandomFileName jest wywoływana w celu wygenerowania bezpiecznej nazwy pliku. Nigdy nie ufaj nazwie pliku podanej przez przeglądarkę, ponieważ cyberattacker może wybrać istniejącą nazwę pliku, która zastępuje istniejący plik lub wysyła ścieżkę, która próbuje zapisać poza aplikacją.

Aplikacja serwera musi rejestrować usługi kontrolera i punkty końcowe kontrolera mapy. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core.

Przekazywanie plików na serwer za pomocą renderowania po stronie klienta (CSR)

Ta sekcja dotyczy składników renderowanych po stronie klienta (CSR) w Blazor Web Appaplikacjach lub s.Blazor WebAssembly

W poniższym przykładzie pokazano przekazywanie plików do internetowego kontrolera interfejsu API zaplecza w oddzielnej aplikacji, na przykład na osobnym serwerze, od składnika Blazor Web App , który przyjmuje csr lub składnik w Blazor WebAssembly aplikacji.

Przykład przyjmuje przesyłanie strumieniowe żądań dla przeglądarki opartej na chromium (na przykład Google Chrome lub Microsoft Edge) przy użyciu protokołu HTTP/2 i PROTOKOŁU HTTPS. Jeśli nie można używać przesyłania strumieniowego żądań, Blazor przełącza się płynnie na Fetch API bez przesyłania strumieniowego żądań. Aby uzyskać więcej informacji, zobacz sekcję Limity odczytu i przekazywania rozmiaru pliku .

Poniższa UploadResult klasa zachowuje wynik przekazanego pliku. Gdy przekazywanie pliku na serwerze nie powiedzie się, zostanie zwrócony ErrorCode kod błędu w celu wyświetlenia użytkownikowi. Bezpieczna nazwa pliku jest generowana na serwerze dla każdego pliku i zwracana do klienta w StoredFileName programie w celu wyświetlenia. Pliki są kluczami między klientem a serwerem przy użyciu niebezpiecznej/niezaufanej nazwy pliku w programie 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; }
}

Uwaga

Poprzednia UploadResult klasa może być współdzielona między projektami klienckimi i serwerowymi. Gdy projekty klienta i serwera współużytkuje klasę, dodaj import do pliku każdego projektu _Imports.razor dla udostępnionego projektu. Na przykład:

@using BlazorSample.Shared

FileUpload2 Następujący składnik:

  • Zezwala użytkownikom na przekazywanie plików z klienta.
  • Wyświetla niezaufaną/niebezpieczną nazwę pliku podaną przez klienta w interfejsie użytkownika. Niezaufana/niebezpieczna nazwa pliku jest automatycznie zakodowana w Razor formacie HTML w celu bezpiecznego wyświetlania w interfejsie użytkownika.

Najlepszym rozwiązaniem w zakresie zabezpieczeń dla aplikacji produkcyjnych jest unikanie wysyłania komunikatów o błędach do klientów, którzy mogą ujawniać poufne informacje o aplikacji, serwerze lub sieci. Udostępnianie szczegółowych komunikatów o błędach może pomóc złośliwego użytkownika w opracowywaniu ataków na aplikację, serwer lub sieć. Przykładowy kod w tej sekcji wysyła tylko numer kodu błędu (int) do wyświetlenia po stronie klienta składnika, jeśli wystąpi błąd po stronie serwera. Jeśli użytkownik wymaga pomocy przy przekazaniu pliku, podaj kod błędu pomocy technicznej dla personelu pomocy technicznej w celu rozwiązania biletu pomocy technicznej bez znajomości dokładnej przyczyny błędu.

Ostrzeżenie

Nie ufaj nazwam plików dostarczonym przez klientów dla:

  • Zapisywanie pliku w systemie plików lub usłudze.
  • Wyświetlaj w interfejsach użytkownika, które nie kodują automatycznie nazw plików ani za pośrednictwem kodu dewelopera.

Aby uzyskać więcej informacji na temat zagadnień dotyczących zabezpieczeń podczas przekazywania plików na serwer, zobacz Przekazywanie plików w programie ASP.NET Core.

W projekcie serwera Blazor Web App, dodaj IHttpClientFactory oraz powiązane usługi w pliku projektu Program.

builder.Services.AddHttpClient();

Usługi HttpClient należy dodać do projektu serwera, ponieważ składnik po stronie klienta jest wstępnie zainstalowany na serwerze. Jeśli wyłączysz prerendering dla następującego składnika, nie musisz zapewniać HttpClient usług w projekcie serwera i nie musisz dodawać poprzedniego wiersza do projektu serwera.

Aby uzyskać więcej informacji na temat dodawania HttpClient usług do aplikacji ASP.NET Core, zobacz Make HTTP requests using IHttpClientFactory in ASP.NET Core (Tworzenie żądań HTTP przy użyciu elementu IHttpClientFactory w programie ASP.NET Core).

Projekt klienta (.Client) Blazor Web App obiektu musi również zarejestrować żądania HttpClient HTTP POST na kontrolerze internetowego interfejsu API zaplecza. Potwierdź lub dodaj następujący kod do pliku projektu Program klienta:

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

W poprzednim przykładzie ustawiono adres podstawowy (builder.HostEnvironment.BaseAddress), który pobiera adres IWebAssemblyHostEnvironment.BaseAddress podstawowy dla aplikacji i jest zwykle pobierany z <base> wartości tagu href na stronie hosta. Jeśli wywołujesz zewnętrzny internetowy interfejs API, ustaw identyfikator URI na adres podstawowy internetowego interfejsu API.

Autonomiczna Blazor WebAssembly aplikacja, która przekazuje pliki do oddzielnego internetowego interfejsu API serwera, używa nazwy HttpClient lub ustawia domyślną HttpClient rejestrację usługi w celu wskazania punktu końcowego internetowego interfejsu API. W poniższym przykładzie, w którym internetowy interfejs API jest hostowany lokalnie na porcie 5001, adres podstawowy to https://localhost:5001:

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

W pliku Blazor Web App dodaj przestrzeń nazw Microsoft.AspNetCore.Components.WebAssembly.Http do dyrektyw składnika.

@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)
        {
            using var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
            request.SetBrowserRequestStreamingEnabled(true);
            request.Content = content;

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

Następujący kontroler w projekcie po stronie serwera zapisuje przekazane pliki z klienta.

Uwaga

Powiązanie wartości formularza z atrybutem[FromForm]nie jest natywnie obsługiwane dla minimalnych interfejsów API w programie ASP.NET Core na platformie .NET 6. W związku z tym nie można przekonwertować następującego Filesave przykładu kontrolera w celu użycia interfejsów API minimalnych. Obsługa powiązań z wartości formularza z minimalnymi interfejsami API jest dostępna w programie ASP.NET Core na platformie .NET 7 lub nowszym.

Aby użyć następującego kodu, utwórz Development/unsafe_uploads folder w katalogu głównym projektu po stronie serwera dla aplikacji działającej Development w środowisku.

Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads folder dla Staging środowiska. Production/unsafe_uploads Utwórz folder dla Production środowiska.

Ostrzeżenie

Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.

W poniższym przykładzie dla hostowanej Blazor WebAssembly aplikacji lub gdy współużytkowany projekt jest używany do dostarczania klasy UploadResult, dodaj przestrzeń nazw współdzielonego projektu:

using BlazorSample.Shared;

Zalecamy użycie przestrzeni nazw dla następującego kontrolera (na przykład: 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);
    }
}

W poprzednim kodzie GetRandomFileName jest wywoływana w celu wygenerowania bezpiecznej nazwy pliku. Nigdy nie ufaj nazwie pliku podanej przez przeglądarkę, ponieważ cyberattacker może wybrać istniejącą nazwę pliku, która zastępuje istniejący plik lub wysyła ścieżkę, która próbuje zapisać poza aplikacją.

Aplikacja serwera musi rejestrować usługi kontrolera i punkty końcowe kontrolera mapy. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core. Zalecamy dodanie usług kontrolera z użyciem AddControllersWithViews w celu automatycznego ograniczenia ryzyka fałszowania żądań między witrynami (XSRF/CSRF) dla uwierzytelnionych użytkowników. Jeśli używasz tylko AddControllers, antyfałszerstwo nie jest włączone automatycznie. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core.

Konfiguracja żądań międzydomenowych (CORS) na serwerze jest wymagana do przesyłania strumieniowego żądań, gdy serwer jest hostowany w innej domenie, a klient zawsze wysyła żądanie wstępne. W konfiguracji pliku serwera Program (projektu serwera internetowego Blazor Web App lub internetowego interfejsu API serwera zaplecza aplikacji Blazor WebAssembly) następująca domyślna polityka CORS jest odpowiednia do testowania z przykładami zawartymi w tym artykule. Klient wysyła żądanie lokalne z portu 5003. Zmień numer portu, aby był zgodny z używanym portem aplikacji klienckiej:

Skonfiguruj żądania między źródłami (CORS) na serwerze. W konfiguracji pliku serwera Program (projektu serwera internetowego Blazor Web App lub internetowego interfejsu API serwera zaplecza aplikacji Blazor WebAssembly) następująca domyślna polityka CORS jest odpowiednia do testowania z przykładami zawartymi w tym artykule. Klient wysyła żądanie lokalne z portu 5003. Zmień numer portu, aby był zgodny z używanym portem aplikacji klienckiej:

Skonfiguruj żądania między źródłami (CORS) na serwerze. W konfiguracji usługi pliku API internetowego serwera zaplecza Program, następująca domyślna polityka CORS jest odpowiednia do testowania przy użyciu przykładów w tym artykule. Klient wysyła żądanie lokalne z portu 5003. Zmień numer portu, aby był zgodny z używanym portem aplikacji klienckiej:

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

Po wywołaniu UseHttpsRedirection w pliku Program, wywołaj UseCors, aby dodać middleware CORS.

app.UseCors();

Aby uzyskać więcej informacji, zobacz Włączanie żądań między źródłami (CORS) w ASP.NET Core.

Skonfiguruj maksymalny rozmiar treści żądania serwera i limity długości treści w wielu częściach, jeśli limity ograniczają rozmiar przekazywania.

Dla serwera Kestrel ustaw MaxRequestBodySize (domyślna wartość: 30 000 000 bajtów) i FormOptions.MultipartBodyLengthLimit (domyślna wartość: 134 217 728 bajtów). Ustaw zmienną maxFileSize w składniku i kontroler na tę samą wartość.

W następującej Program konfiguracji pliku Kestrel (serwera projektu Blazor Web App lub internetowego interfejsu API aplikacji serwera zaplecza Blazor WebAssembly) symbol zastępczy {LIMIT} oznacza limit w bajtach:

using Microsoft.AspNetCore.Http.Features;

...

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

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

Anulowanie przekazywania pliku

Składnik przekazywania plików może wykryć, kiedy użytkownik anulował przekazywanie przy użyciu CancellationToken elementu podczas wywoływania elementu IBrowserFile.OpenReadStream lub StreamReader.ReadAsync.

Utwórz element CancellationTokenSource dla InputFile składnika. Na początku OnInputFileChange metody sprawdź, czy poprzednie przekazywanie jest w toku.

Jeśli przekazywanie pliku jest w toku:

Przekazywanie plików po stronie serwera z postępem

W poniższym przykładzie pokazano, jak przekazywać pliki w aplikacji po stronie serwera z postępem przekazywania wyświetlanym użytkownikowi.

Aby użyć następującego przykładu w aplikacji testowej:

  • Utwórz folder do zapisania przekazanych plików dla Development środowiska: Development/unsafe_uploads.
  • Skonfiguruj maksymalny rozmiar pliku (maxFileSize15 KB w poniższym przykładzie) i maksymalną liczbę dozwolonych plików (maxAllowedFiles3 w poniższym przykładzie).
  • Ustaw bufor na inną wartość (10 KB w poniższym przykładzie), jeśli jest to konieczne, aby zwiększyć stopień szczegółowości w toku raportowania. Nie zalecamy używania buforu większego niż 30 KB z powodu problemów z wydajnością i zabezpieczeniami.

Ostrzeżenie

Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie 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;
    }
}

Aby uzyskać więcej informacji, zobacz następujące zasoby interfejsu API:

  • FileStream: udostępnia Stream element dla pliku, który obsługuje zarówno operacje synchroniczne, jak i asynchroniczne operacji odczytu i zapisu.
  • FileStream.ReadAsync: Poprzedni FileUpload3 składnik odczytuje strumień asynchronicznie za pomocą polecenia ReadAsync. Odczytywanie strumienia synchronicznie z elementem Read nie jest obsługiwane w Razor składnikach.

Strumienie plików

W przypadku interakcyjności serwera dane plików są przesyłane strumieniowo za pośrednictwem SignalR połączenia z kodem platformy .NET na serwerze, ponieważ plik jest odczytywany.

RemoteBrowserFileStreamOptions umożliwia konfigurowanie właściwości przekazywania plików.

W przypadku składnika renderowanego za pomocą zestawu WebAssembly dane plików są przesyłane strumieniowo bezpośrednio do kodu platformy .NET w przeglądarce.

Przekazywanie podglądu obrazu

Aby uzyskać podgląd obrazu przekazywania obrazów, zacznij od dodania InputFile składnika z odwołaniem do składnika i procedurą OnChange obsługi:

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

Dodaj element obrazu z odwołaniem do elementu, który służy jako symbol zastępczy podglądu obrazu:

<img @ref="previewImageElem" />

Dodaj skojarzone odwołania:

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

W języku JavaScript dodaj funkcję o nazwie z kodem HTML input i img elementem, który wykonuje następujące czynności:

  • Wyodrębnia wybrany plik.
  • Tworzy adres URL obiektu za pomocą polecenia createObjectURL.
  • Ustawia odbiornik zdarzeń, aby odwołać adres URL obiektu za pomocą revokeObjectURL polecenia po załadowaniu obrazu, więc pamięć nie zostanie ujawniona.
  • img Ustawia źródło elementu, aby wyświetlić obraz.
window.previewImage = (inputElem, imgElem) => {
  const url = URL.createObjectURL(inputElem.files[0]);
  imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
  imgElem.src = url;
}

Na koniec użyj wstrzykniętego IJSRuntime elementu , aby dodać procedurę OnChange obsługi, która wywołuje funkcję JavaScript:

@inject IJSRuntime JS

...

@code {
    ...

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

Powyższy przykład dotyczy przekazywania pojedynczego obrazu. Podejście można rozszerzyć w celu obsługi multiple obrazów.

FileUpload4 Poniższy składnik przedstawia kompletny przykład.

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

Zapisywanie małych plików bezpośrednio w bazie danych za pomocą polecenia EF Core

Wiele aplikacji ASP.NET Core używa programu Entity Framework Core (EF Core) do zarządzania operacjami bazy danych. Zapisywanie miniatur i awatarów bezpośrednio w bazie danych jest typowym wymaganiem. W tej sekcji przedstawiono ogólne podejście, które można dodatkowo ulepszyć w przypadku aplikacji produkcyjnych.

Następujący wzorzec:

  • Jest oparta na aplikacji do nauki korzystania z bazy danych filmów.
  • Można go ulepszyć przy użyciu dodatkowego kodu w celu uzyskania informacji zwrotnych dotyczących rozmiaru pliku i weryfikacji typu zawartości.
  • Wiąże się z pogorszeniem wydajności i ryzykiem DoS. Ostrożnie należy rozważyć ryzyko podczas odczytywania dowolnego pliku do pamięci i rozważyć alternatywne podejścia, szczególnie w przypadku większych plików. Alternatywne podejścia obejmują zapisywanie plików bezpośrednio na dysku lub w usłudze innej firmy na potrzeby kontroli oprogramowania antywirusowego/ochrony przed złośliwym kodem, dalszego przetwarzania i obsługi klientów.

Aby poniższy przykład Blazor Web App działał poprawnie w (.NET 8 lub nowszym), składnik musi przyjąć interaktywny tryb renderowania (na przykład @rendermode InteractiveServer) w celu wywołania HandleSelectedThumbnail przy zmianie pliku składnika InputFile (OnChange parametr/zdarzenie). Blazor Server składniki aplikacji są zawsze interaktywne i nie wymagają trybu renderowania.

W poniższym przykładzie mała miniatura (<= 100 KB) w obiekcie IBrowserFile jest zapisywana w bazie danych za pomocą polecenia EF Core. Jeśli plik nie zostanie wybrany przez użytkownika dla InputFile składnika, domyślna miniatura zostanie zapisana w bazie danych.

Domyślna miniatura (default-thumbnail.jpg) znajduje się w katalogu głównym projektu z ustawieniem Kopiuj do katalogu wyjściowegoKopiuj, jeśli nowsze:

Domyślny ogólny obraz miniatury

Model Movie () ma właściwość (Movie.csThumbnail) do przechowywania danych obrazu miniatury:

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

Dane obrazu są przechowywane jako bajty w bazie danych jako varbinary(MAX). Aplikacja base-64 koduje bajty do wyświetlenia, ponieważ zakodowane w formacie base-64 dane są mniej więcej jedną trzecią większą niż nieprzetworzone bajty obrazu, dlatego dane obrazu base-64 wymagają dodatkowego magazynu bazy danych i zmniejszają wydajność operacji odczytu/zapisu bazy danych.

Składniki, które wyświetlają dane obrazu miniatury, przekazują dane obrazu do atrybutu tagu imgsrc jako dane zakodowane w formacie JPEG, base-64:

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

W poniższym Create składniku następuje przetwarzanie przesyłania obrazu. Możesz jeszcze bardziej ulepszyć przykład dzięki niestandardowej weryfikacji typu i rozmiaru pliku przy użyciu metod weryfikacji formularzy ASP.NET CoreBlazor. Aby zobaczyć pełny składnik Create bez kodu przesyłania miniatur w poniższym przykładzie, zobacz aplikację przykładową BlazorWebAppMovies w repozytorium GitHub z przykładami Blazor.

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

Takie samo podejście byłoby przyjęte w składniku Edit z trybem renderowania interakcyjnego, jeśli użytkownicy mogli edytować obraz miniatury filmu.

Przekazywanie plików do usługi zewnętrznej

Zamiast aplikacji obsługującej bajty przekazywania plików i serwera aplikacji odbierającego przekazane pliki, klienci mogą bezpośrednio przekazywać pliki do usługi zewnętrznej. Aplikacja może bezpiecznie przetwarzać pliki z usługi zewnętrznej na żądanie. Takie podejście wzmacnia zabezpieczenia aplikacji i jej serwera przed złośliwymi atakami i potencjalnymi problemami z wydajnością.

Rozważmy podejście korzystające z usługi Azure Files, Azure Blob Storage lub usługi innej firmy z następującymi potencjalnymi korzyściami:

Aby uzyskać więcej informacji na temat usług Azure Blob Storage i Azure Files, zobacz dokumentację usługi Azure Storage.

Limit rozmiaru komunikatów po stronie SignalR serwera

Przekazywanie plików może zakończyć się niepowodzeniem nawet przed rozpoczęciem, gdy Blazor pobiera dane dotyczące plików, które przekraczają maksymalny SignalR rozmiar komunikatu.

SignalR Definiuje limit rozmiaru komunikatów, który ma zastosowanie do każdego odbieranego komunikatu Blazor , a InputFile składnik przesyła strumieniowo pliki do serwera w komunikatach, które przestrzegają skonfigurowanego limitu. Jednak pierwszy komunikat, który wskazuje zestaw plików do przekazania, jest wysyłany jako unikatowy pojedynczy komunikat. Rozmiar pierwszego komunikatu może przekroczyć limit rozmiaru komunikatu SignalR . Problem nie jest związany z rozmiarem plików, jest związany z liczbą plików.

Zarejestrowany błąd jest podobny do następującego:

Błąd: Połączenie zostało rozłączone z powodu błędu "Błąd: Serwer zwrócił błąd podczas zamykania: połączenie zostało zamknięte z powodu błędu". e.log @ blazor.server.js:1

Podczas przekazywania plików osiągnięcie limitu rozmiaru komunikatu dla pierwszego komunikatu jest rzadkie. Jeśli limit zostanie osiągnięty, aplikacja może skonfigurować HubOptions.MaximumReceiveMessageSize z większą wartością.

Aby uzyskać więcej informacji na SignalR temat konfiguracji i sposobu ustawiania MaximumReceiveMessageSizeprogramu , zobacz Blazor dotyczące platformy ASP.NET CoreSignalR.

Maksymalna liczba wywołań równoległych na ustawienie centrum klienta

Blazor wartość jest ustawiona na MaximumParallelInvocationsPerClient wartość 1, która jest wartością domyślną.

Zwiększenie wartości prowadzi do wysokiego prawdopodobieństwa, że CopyTo operacje zgłaszają wartość System.InvalidOperationException: 'Reading is not allowed after reader was completed.'. Aby uzyskać więcej informacji, zobacz MaximumParallelInvocationsPerClient > 1 przerywa przekazywanie plików w Blazor Server trybie (dotnet/aspnetcore #53951).

Rozwiązywanie problemów

Wiersz, który wywołuje IBrowserFile.OpenReadStream element , zgłasza element System.TimeoutException:

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

Możliwe przyczyny:

Dodatkowe zasoby