Partager via


Chargements de fichiers Blazor ASP.NET Core

Note

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.

Warning

Cette version d'ASP.NET Core n'est plus prise en charge. Pour plus d’informations, consultez la stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 de cet article.

Importante

Ces informations portent sur la préversion du produit, qui est susceptible d'être en grande partie modifié avant sa commercialisation. Microsoft n'offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 9 de cet article.

Cet article explique comment charger des fichiers dans Blazor avec le composant InputFile.

Chargements de fichiers

Warning

Suivez toujours les bonnes pratiques de sécurité quand il s’agit d’autoriser des utilisateurs à charger des fichiers. Pour plus d’informations, consultez Charger des fichiers dans ASP.NET Core.

Utilisez le composant InputFile pour lire des données de fichiers de navigateur dans du code .NET. Le composant InputFile rend un élément HTML <input> de type file pour les chargements de fichiers uniques. Ajoutez l’attribut multiple pour autoriser l’utilisateur à charger plusieurs fichiers à la fois.

La sélection de fichiers n’est pas cumulative quand un composant InputFile ou son élément HTML <input type="file"> sous-jacent est utilisé. Vous ne pouvez donc pas ajouter de fichiers à une sélection de fichiers existante. Le composant remplace toujours la sélection de fichier initiale de l’utilisateur, si bien que les références de fichiers des sélections précédentes ne sont pas disponibles.

Le composant InputFile suivant exécute la méthode LoadFiles quand l’événement OnChange (change) se produit. Un InputFileChangeEventArgs donne accès à la liste de fichiers sélectionnée et aux détails sur chaque fichier :

<InputFile OnChange="LoadFiles" multiple />

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

HTML rendu :

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

Note

Dans l’exemple précédent, l’attribut <input> de l’élément _bl_2 est utilisé pour le traitement interne de Blazor.

Pour lire des données à partir d’un fichier sélectionné par l’utilisateur avec un Stream qui représente les octets du fichier, appelez IBrowserFile.OpenReadStream le fichier et lisez à partir du flux retourné. Pour plus d’informations, consultez la section Flux de fichiers.

OpenReadStreamapplique une taille maximale en octets à son Stream. La lecture d’un ou plusieurs fichiers de plus de 500 Ko provoque une exception. Cette limite empêche les développeurs de lire accidentellement de gros fichiers en mémoire. Le paramètre maxAllowedSize de OpenReadStream peut être utilisé pour spécifier une plus grande taille si nécessaire.

En dehors du traitement d’un petit fichier, évitez de lire le flux de fichiers entrant directement en mémoire en même temps. Par exemple, ne copiez pas tous les octets du fichier dans un MemoryStream ou ne lisez pas le flux entier dans un tableau d’octets simultanément. Ces approches peuvent entraîner une dégradation des performances de l’application et un risque potentiel de déni de service (DoS), en particulier pour les composants côté serveur. À la place, vous pouvez adopter l’une des approches suivantes :

  • Copiez le flux directement dans un fichier sur le disque sans le lire en mémoire. Notez que les applications Blazor exécutant du code sur le serveur ne peuvent pas accéder directement au système de fichiers du client.
  • Chargez les fichiers du client directement dans un service externe. Pour plus d’informations, consultez la section Charger des fichiers dans un service externe.

Dans les exemples suivants, browserFile implémente IBrowserFile pour représenter un fichier chargé. Des implémentations fonctionnelles pour IBrowserFile sont présentées dans les composants de chargement de fichiers plus loin dans cet article.

Lors de l’appel OpenReadStream, nous vous recommandons de passer une taille de fichier maximale autorisée dans le maxAllowedSize paramètre à la limite des tailles de fichier que vous prévoyez de recevoir. La valeur par défaut est de 500 Ko. Les exemples de cet article utilisent une variable de taille de fichier maximale autorisée ou une constante nommée maxFileSize , mais n’affichent généralement pas la définition d’une valeur spécifique.

Prise en charge : L’approche suivante est recommandée, car le Stream du fichier est fourni directement au consommateur, un FileStream qui crée le fichier à l’emplacement fourni :

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

️Pris en charge : L’approche suivante est recommandée pour Stockage Blob Microsoft Azure, car le Stream du fichier est fourni directement à UploadBlobAsync :

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

Recommandé uniquement pour les petits fichiers : l’approche suivante est recommandée uniquement pour les petits fichiers , car le contenu du Stream fichier est lu en MemoryStream mémoire (memoryStream), ce qui entraîne une pénalité de performances et un risque DoS . Pour obtenir un exemple illustrant cette technique pour enregistrer une image miniature dans IBrowserFile à une base de données à l’aide d’Entity Framework Core (EF Core), consultez la section Enregistrer les petits fichiers directement dans une base de données avec EF Core plus loin dans cet article.

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

Non recommandé : l’approche suivante n’est PAS recommandée, car le contenu Stream du fichier est lu dans un String en mémoire (reader) :

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

Non recommandé : l’approche suivante n’est PAS recommandée pour Microsoft Azure Blob Storage, car le contenu Stream du fichier est copié dans un MemoryStream en mémoire (memoryStream) avant d’appeler UploadBlobAsync :

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

Un composant qui reçoit un fichier image peut appeler la méthode pratique BrowserFileExtensions.RequestImageFileAsync sur le fichier pour redimensionner les données d’image dans le runtime JavaScript du navigateur avant que l’image soit diffusée dans l’application. Les cas d’usage pour l’appel de RequestImageFileAsync sont plus appropriés pour les applications Blazor WebAssembly.

Utilisateurs du conteneur Autofac Inversion of Control (IoC)

Si vous utilisez le conteneur Autofac Inversion of Control (IoC) au lieu du conteneur d’injection de dépendances ASP.NET Core intégré, définissez DisableImplicitFromServicesParameters sur true dans les options du hub du gestionnaire de circuit côté serveur. Pour plus d’informations, consultez FileUpload : aucune donnée n’a été reçue dans le délai imparti (dotnet/aspnetcore #38842).

Limites de taille de fichier en lecture et chargement

Pour les navigateurs Chromium (par exemple, Google Chrome et Microsoft Edge) utilisant le protocole HTTP/2, HTTPS et CORS, du côté Blazor client prend en charge l’utilisation de l’API Streams pour permettre le téléversement de fichiers volumineux avec la diffusion en continu des requêtes.

Sans navigateur Chromium, protocole HTTP/2 ou HTTPS, le côté client Blazor lit les octets du fichier dans un seul tampon de tableau JavaScript lors du marshaling des données de JavaScript vers C#, qui est limité à 2 Go ou à la mémoire disponible de l’appareil. Les téléversements de fichiers volumineux peuvent échouer avec le composant InputFile pour les téléversements côté client.

Côté Blazor client, les octets du fichier sont lus dans un tampon mémoire JavaScript unique lors du transfert des données de JavaScript vers C#. Ce processus est limité à 2 Go ou à la mémoire disponible de l’appareil. Les téléversements de fichiers volumineux peuvent échouer avec le composant InputFile pour les téléversements côté client. Nous vous recommandons d’adopter le streaming des demandes avec .NET 9 ou version ultérieure.

Considérations relatives à la sécurité

Évitez IBrowserFile.Size pour les limites de taille de fichier

Évitez d’utiliser IBrowserFile.Size pour imposer une limite à la taille du fichier. Au lieu d’utiliser la taille de fichier fournie par le client non sécurisé, spécifiez explicitement la taille de fichier maximale. L’exemple suivant utilise la taille maximale de fichier affectée à maxFileSize:

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

Sécurité du nom de fichier

N’utilisez jamais de nom de fichier fourni par le client pour enregistrer un fichier dans le stockage physique. Créez un nom de fichier sécurisé pour le fichier à l’aide Path.GetRandomFileName() ou Path.GetTempFileName() pour créer un chemin d’accès complet (y compris le nom de fichier) pour le stockage temporaire.

Razor encode automatiquement en HTML les valeurs de propriété pour l’affichage. Le code suivant est sûr à utiliser :

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

En dehors de Razor, utilisez HtmlEncode toujours pour encoder en toute sécurité les noms de fichiers à partir de la demande d’un utilisateur.

De nombreuses implémentations doivent inclure une vérification que le fichier existe ; sinon, le fichier est remplacé par un fichier du même nom. Fournissez une logique supplémentaire pour répondre aux spécifications de votre application.

Exemples

Les exemples suivants illustrent le chargement de plusieurs fichiers dans un composant. InputFileChangeEventArgs.GetMultipleFilespermet de lire plusieurs fichiers. Spécifiez le nombre maximal de fichiers pour empêcher un utilisateur malveillant de charger un nombre de fichiers supérieur à celui attendu par l’application. InputFileChangeEventArgs.Filepermet de lire le premier et le seul fichier si le chargement de fichiers ne prend pas en charge plusieurs fichiers.

InputFileChangeEventArgs se trouve dans l’espace de noms Microsoft.AspNetCore.Components.Forms, qui est généralement l’un des espaces de noms du fichier _Imports.razor de l’application. Lorsque l’espace de noms est présent dans le fichier _Imports.razor, il fournit un accès aux membres API aux composants de l’application.

Les espaces de noms contenus dans le fichier _Imports.razor ne s’appliquent pas aux fichiers C# (.cs). Les fichiers C# nécessitent une directive using explicite en haut du fichier de classe :

using Microsoft.AspNetCore.Components.Forms;

Pour tester les composants de chargement de fichiers, vous pouvez créer des fichiers de test de toute taille avec PowerShell :

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

Dans la commande précédente :

  • L’espace réservé {SIZE} correspond à la taille du fichier en octets (par exemple, 2097152 pour un fichier de 2 Mo).
  • L’espace réservé {PATH} correspond au chemin et au fichier avec l’extension de fichier (par exemple, D:/test_files/testfile2MB.txt).

Exemple de chargement de fichiers côté serveur

Pour utiliser le code suivant, créez un dossier Development/unsafe_uploads à la racine de l’application s’exécutant dans l’environnement Development.

Sachant que l’exemple utilise l’environnement de l’application dans le chemin où sont enregistrés les fichiers, des dossiers supplémentaires sont nécessaires si d’autres environnements sont utilisés en phase de test et en production. Par exemple, créez un dossier Staging/unsafe_uploads pour l’environnement Staging. Créez un dossier Production/unsafe_uploads pour l’environnement Production.

Warning

L’exemple enregistre des fichiers sans analyser leur contenu, et l’aide fournie dans cet article ne prend pas en compte les autres bonnes pratiques de sécurité pour les fichiers chargés. Sur les systèmes de mise en lots et de production, désactivez l’autorisation d’exécution sur le dossier de chargement et analysez les fichiers avec une API d’analyseur antivirus/anti-programme malveillant de suite après le chargement. Pour plus d’informations, consultez Charger des fichiers dans 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;
    }
}

Exemple de chargement de fichiers côté client

L’exemple suivant traite les octets de fichiers et n’envoie pas de fichiers vers une destination en dehors de l’application. Pour trouverez un exemple de composant Razor qui envoie un fichier à un serveur ou un service dans les sections suivantes :

Le composant suppose que le mode de rendu Interactive WebAssembly (InteractiveWebAssembly) est hérité d’un composant parent ou appliqué globalement à l’application.

@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 renvoie les métadonnées exposées par le navigateur en tant que propriétés. Utilisez ces métadonnées pour la validation préliminaire.

Ne faites jamais confiance aux valeurs des propriétés précédentes, en particulier à la propriété Name pour l’affichage dans l’interface utilisateur. Traitez toutes les données fournies par l’utilisateur comme un risque de sécurité important pour l’application, le serveur et le réseau. Pour plus d’informations, consultez Charger des fichiers dans ASP.NET Core.

Chargement de fichiers vers un serveur avec rendu côté serveur

Cette section s’applique aux composants Interactive Server dans les applications Blazor Web Apps ou Blazor Server.

L’exemple suivant illustre le chargement de fichiers depuis une application côté serveur vers un contrôleur API Web back-kend dans une application distincte, éventuellement sur un serveur distinct.

Dans le fichier Program de l’application côté serveur, ajoutez IHttpClientFactory et les services associés qui permettent à l’application de créer des instances HttpClient :

builder.Services.AddHttpClient();

Pour plus d’informations, consultez Effectuer des requêtes HTTP en utilisant IHttpClientFactory dans ASP.NET Core.

Pour les exemples de cette section :

  • L’API web s’exécute à l’URL : https://localhost:5001
  • L’application côté serveur s’exécute à l’URL : https://localhost:5003

Pour les tests, les URL précédentes sont configurées dans les fichiers Properties/launchSettings.json des projets.

La classe UploadResult suivante conserve le résultat d’un fichier chargé. Quand le chargement d’un fichier échoue sur le serveur, un code d’erreur est retourné dans ErrorCode pour l’afficher à l’utilisateur. Un nom de fichier sécurisé est généré sur le serveur pour chaque fichier et retourné au client dans StoredFileName pour affichage. Une clé est ajoutée aux fichiers entre le client et le serveur en utilisant le nom de fichier non sécurisé/non approuvé dans 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; }
}

Pour les applications de production, une bonne pratique de sécurité consiste à éviter d’envoyer des messages d’erreur aux clients qui pourraient divulguer des informations sensibles sur une application, un serveur ou un réseau. Des messages d’erreur détaillés entre les mains d’un utilisateur malveillant peuvent l’aider à préparer des attaques sur une application, un serveur ou un réseau. L’exemple de code figurant dans cette section renvoie uniquement un numéro de code d’erreur (int) qui sera affiché par le composant côté client si une erreur côté serveur se produit. Si un utilisateur a besoin d’une assistance par rapport à un chargement de fichier, il fournira le code d’erreur au personnel du support technique pour la résolution du ticket de support sans jamais connaître la cause exacte de l’erreur.

La classe LazyBrowserFileStream suivante définit un type de flux personnalisé qui appelle OpenReadStream de manière différée juste avant que les premiers octets du flux ne soient requis. Le flux n’est pas transmis du navigateur au serveur tant que la lecture du flux n’a pas commencé dans .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();
}

Le composant FileUpload2 suivant :

  • Permet aux utilisateurs de charger des fichiers depuis le client.
  • Affiche le nom de fichier non approuvé/non sécurisé fourni par le client dans l’interface utilisateur. Le nom de fichier non approuvé/non sécurisé est automatiquement encodé au format HTML par Razor pour un affichage sécurisé dans l’interface utilisateur.

Warning

Ne faites pas confiance aux noms de fichiers fournis par les clients pour :

  • L’enregistrement du fichier dans un système de fichiers ou un service.
  • Un affichage dans des interfaces utilisateur qui n’encodent pas les noms de fichiers automatiquement ou via le code du développeur.

Pour plus d’informations sur les considérations de sécurité lors du chargement de fichiers sur un serveur, consultez Charger des fichiers dans 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; }
    }
}

Si le composant limite les chargements à un seul fichier à la fois ou s'il adopte uniquement le rendu côté client (CSR, InteractiveWebAssembly), le composant peut éviter l'utilisation de LazyBrowserFileStream et utiliser un Stream. L’exemple suivant illustre les modifications apportées au composant FileUpload2 :

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

Supprimez la classe LazyBrowserFileStream (LazyBrowserFileStream.cs), car elle n’est pas utilisée.

Si le composant limite les chargements de fichiers à un seul fichier à la fois, il peut éviter l’utilisation de LazyBrowserFileStream et utiliser un Stream. L’exemple suivant illustre les modifications apportées au composant FileUpload2 :

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

Supprimez la classe LazyBrowserFileStream (LazyBrowserFileStream.cs), car elle n’est pas utilisée.

Le contrôleur ci-dessous du projet d’API web enregistre les fichiers chargés depuis le client.

Importante

Le contrôleur de cette section est destiné à être utilisé dans un projet d’API web distinct de l’application Blazor. L’API Web doit atténuer les attaques de type Cross-Site Request Forgery (XSRF/CSRF) si les utilisateurs qui chargent des fichiers sont authentifiés.

Note

La liaison des valeurs de formulaire avec l’attribut [FromForm] n’est pas prise en charge de manière native pour les API minimales dans ASP.NET Core dans .NET 6. Par conséquent, l’exemple de contrôleur Filesave ci-dessous ne peut pas être converti pour utiliser les API minimales. La prise en charge de la liaison à partir des valeurs de formulaire avec les API minimales est disponible dans ASP.NET Core dans .NET 7 ou une version ultérieure.

Pour utiliser le code suivant, créez un dossier Development/unsafe_uploads à la racine du projet d’API Web pour l’application exécutée dans l’environnement Development.

Sachant que l’exemple utilise l’environnement de l’application dans le chemin où sont enregistrés les fichiers, des dossiers supplémentaires sont nécessaires si d’autres environnements sont utilisés en phase de test et en production. Par exemple, créez un dossier Staging/unsafe_uploads pour l’environnement Staging. Créez un dossier Production/unsafe_uploads pour l’environnement Production.

Warning

L’exemple enregistre des fichiers sans analyser leur contenu, et l’aide fournie dans cet article ne prend pas en compte les autres bonnes pratiques de sécurité pour les fichiers chargés. Sur les systèmes de mise en lots et de production, désactivez l’autorisation d’exécution sur le dossier de chargement et analysez les fichiers avec une API d’analyseur antivirus/anti-programme malveillant de suite après le chargement. Pour plus d’informations, consultez Charger des fichiers dans 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);
    }
}

Dans le code précédent, GetRandomFileName est appelé pour générer un nom de fichier sécurisé. Ne faites jamais confiance au nom de fichier fourni par le navigateur, car un cyberattaquant peut choisir un nom de fichier existant qui écrase un fichier existant ou envoyer un chemin qui tente d’écrire en dehors de l’application.

L’application serveur doit enregistrer les services de contrôleur et mapper les points de terminaison du contrôleur. Pour plus d’informations, consultez Routage vers des actions de contrôleur dans ASP.NET Core.

Charger des fichiers sur un serveur avec rendu côté client (CSR)

Cette section s’applique aux composants rendus côté client (CSR) dans les Blazor Web App ou les applications Blazor WebAssembly.

L’exemple suivant montre comment charger des fichiers vers un contrôleur d’API Web de backend dans une application séparée, éventuellement sur un serveur séparé, à partir d’un composant dans une application Blazor Web App qui adopte CSR ou d’un composant dans une application Blazor WebAssembly.

L’exemple adopte le streaming de demandes pour un navigateur basé sur Chromium (par exemple, Google Chrome ou Microsoft Edge) avec le protocole HTTP/2 et HTTPS. Si le streaming de requêtes ne peut pas être utilisé, Blazor passe en douceur à l’API Fetch sans streaming de requêtes. Pour plus d’informations, consultez la section Des limites de lecture et de chargement de la taille de fichier .

La classe UploadResult suivante conserve le résultat d’un fichier chargé. Quand le chargement d’un fichier échoue sur le serveur, un code d’erreur est retourné dans ErrorCode pour l’afficher à l’utilisateur. Un nom de fichier sécurisé est généré sur le serveur pour chaque fichier et retourné au client dans StoredFileName pour affichage. Une clé est ajoutée aux fichiers entre le client et le serveur en utilisant le nom de fichier non sécurisé/non approuvé dans FileName.

UploadResult.cs :

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

Note

La classe UploadResult précédente peut être partagée entre des projets basés sur le client et sur le serveur. Lorsque les projets client et serveur partagent la classe, ajoutez une importation au fichier _Imports.razor de chaque projet pour le projet partagé. Par exemple :

@using BlazorSample.Shared

Le composant FileUpload2 suivant :

  • Permet aux utilisateurs de charger des fichiers depuis le client.
  • Affiche le nom de fichier non approuvé/non sécurisé fourni par le client dans l’interface utilisateur. Le nom de fichier non approuvé/non sécurisé est automatiquement encodé au format HTML par Razor pour un affichage sécurisé dans l’interface utilisateur.

Pour les applications de production, une bonne pratique de sécurité consiste à éviter d’envoyer des messages d’erreur aux clients qui pourraient divulguer des informations sensibles sur une application, un serveur ou un réseau. Des messages d’erreur détaillés entre les mains d’un utilisateur malveillant peuvent l’aider à préparer des attaques sur une application, un serveur ou un réseau. L’exemple de code figurant dans cette section renvoie uniquement un numéro de code d’erreur (int) qui sera affiché par le composant côté client si une erreur côté serveur se produit. Si un utilisateur a besoin d’une assistance par rapport à un chargement de fichier, il fournira le code d’erreur au personnel du support technique pour la résolution du ticket de support sans jamais connaître la cause exacte de l’erreur.

Warning

Ne faites pas confiance aux noms de fichiers fournis par les clients pour :

  • L’enregistrement du fichier dans un système de fichiers ou un service.
  • Un affichage dans des interfaces utilisateur qui n’encodent pas les noms de fichiers automatiquement ou via le code du développeur.

Pour plus d’informations sur les considérations de sécurité lors du chargement de fichiers sur un serveur, consultez Charger des fichiers dans ASP.NET Core.

Dans le projet de serveur Blazor Web App, ajoutez IHttpClientFactory et les services connexes dans le fichier du projet Program.

builder.Services.AddHttpClient();

Les HttpClient services doivent être ajoutés au projet de serveur, car le composant côté client est pré-rendu sur le serveur. Si vous désactivez le prérendering pour le composant suivant, vous n’êtes pas obligé de fournir les HttpClient services dans le projet serveur et n’avez pas besoin d’ajouter la ligne précédente au projet serveur.

Pour plus d’informations sur l’ajout de services HttpClient à une application ASP.NET Core, consultez Effectuer des requêtes HTTP à l’aide de IHttpClientFactory dans ASP.NET Core.

Le projet client (.Client) d’un Blazor Web App doit également enregistrer un HttpClient pour les requêtes HTTP POST vers un contrôleur d’API Web de backend. Vérifiez ou ajoutez ce qui suit au fichier Program du projet client :

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

L’exemple précédent définit l’adresse de base avec builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress), qui obtient l’adresse de base de l’application et est généralement dérivée de la valeur <base> de la balise href dans la page hôte. Si vous appelez une API Web externe, définissez l’URI sur l’adresse de base de l’API Web.

Une application Blazor WebAssembly autonome qui charge des fichiers vers une API Web de serveur distincte utilise soit un HttpClient nommé, soit définit l’enregistrement de service HttpClient par défaut pour pointer vers le point de terminaison de l’API Web. Dans l’exemple suivant où l’API web est hébergée localement sur le port 5001, l’adresse de base est https://localhost:5001:

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

Dans un Blazor Web App, ajoutez l’espace de noms Microsoft.AspNetCore.Components.WebAssembly.Http aux directives du composant :

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

Le contrôleur suivant dans le projet côté serveur enregistre les fichiers chargés à partir du client.

Note

La liaison des valeurs de formulaire avec l’attribut [FromForm] n’est pas prise en charge de manière native pour les API minimales dans ASP.NET Core dans .NET 6. Par conséquent, l’exemple de contrôleur Filesave ci-dessous ne peut pas être converti pour utiliser les API minimales. La prise en charge de la liaison à partir des valeurs de formulaire avec les API minimales est disponible dans ASP.NET Core dans .NET 7 ou une version ultérieure.

Pour utiliser le code suivant, créez un dossier Development/unsafe_uploads à la racine du projet côté serveur pour l’application s’exécutant dans l’environnement Development.

Sachant que l’exemple utilise l’environnement de l’application dans le chemin où sont enregistrés les fichiers, des dossiers supplémentaires sont nécessaires si d’autres environnements sont utilisés en phase de test et en production. Par exemple, créez un dossier Staging/unsafe_uploads pour l’environnement Staging. Créez un dossier Production/unsafe_uploads pour l’environnement Production.

Warning

L’exemple enregistre des fichiers sans analyser leur contenu, et l’aide fournie dans cet article ne prend pas en compte les autres bonnes pratiques de sécurité pour les fichiers chargés. Sur les systèmes de mise en lots et de production, désactivez l’autorisation d’exécution sur le dossier de chargement et analysez les fichiers avec une API d’analyseur antivirus/anti-programme malveillant de suite après le chargement. Pour plus d’informations, consultez Charger des fichiers dans ASP.NET Core.

Dans l’exemple suivant pour une application hébergée Blazor WebAssembly ou où un projet partagé est utilisé pour fournir la UploadResult classe, ajoutez l’espace de noms du projet partagé :

using BlazorSample.Shared;

Nous vous recommandons d’utiliser un espace de noms pour le contrôleur suivant (par exemple : 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);
    }
}

Dans le code précédent, GetRandomFileName est appelé pour générer un nom de fichier sécurisé. Ne faites jamais confiance au nom de fichier fourni par le navigateur, car un cyberattaquant peut choisir un nom de fichier existant qui écrase un fichier existant ou envoyer un chemin qui tente d’écrire en dehors de l’application.

L’application serveur doit enregistrer les services de contrôleur et mapper les points de terminaison du contrôleur. Pour plus d’informations, consultez Routage vers des actions de contrôleur dans ASP.NET Core. Nous recommandons d’ajouter des services de contrôleur avec AddControllersWithViews afin d’atténuer automatiquement les attaques de falsification de requêtes intersites (XSRF/CSRF) pour les utilisateurs authentifiés. Si vous utilisez simplement AddControllers, l’antiforgery n'est pas activé automatiquement. Pour plus d’informations, consultez Routage vers des actions de contrôleur dans ASP.NET Core.

La configuration des demandes d’origine croisée (CORS) sur le serveur est requise pour la diffusion en continu des demandes lorsque le serveur est hébergé à une autre origine et qu’une demande préliminaire est toujours effectuée par le client. Dans la configuration du service du fichier Program du serveur (le projet serveur d’un Blazor Web App ou l’API Web du serveur back-kend d’une application Blazor WebAssembly), la stratégie CORS par défaut suivante convient pour les tests avec les exemples de cet article. Le client effectue la requête locale à partir du port 5003. Modifiez le numéro de port pour qu’il corresponde au port de l’application cliente que vous utilisez :

Configurez les demandes d’origine croisée (CORS) sur le serveur. Dans la configuration du service du fichier Program du serveur (le projet serveur d’un Blazor Web App ou l’API Web du serveur back-kend d’une application Blazor WebAssembly), la stratégie CORS par défaut suivante convient pour les tests avec les exemples de cet article. Le client effectue la requête locale à partir du port 5003. Modifiez le numéro de port pour qu’il corresponde au port de l’application cliente que vous utilisez :

Configurez les demandes d’origine croisée (CORS) sur le serveur. Dans la configuration du fichier Program de l'API web sur le serveur principal, la politique CORS par défaut suivante est adaptée pour tester les exemples de cet article. Le client effectue la requête locale à partir du port 5003. Modifiez le numéro de port pour qu’il corresponde au port de l’application cliente que vous utilisez :

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

Après avoir appelé UseHttpsRedirection dans le fichier Program, appelez UseCors pour ajouter un intergiciel CORS :

app.UseCors();

Pour plus d’informations, consultez Activer les demandes d’origine croisée (CORS) dans ASP.NET Core.

Configurez la taille maximale du corps de la requête et les limites de longueur du corps multipart du serveur si les limites limitent la taille du chargement.

Pour le Kestrel serveur, définissez MaxRequestBodySize (valeur par défaut : 30 000 000 octets) et FormOptions.MultipartBodyLengthLimit (valeur par défaut : 134 217 728 octets). Définissez la maxFileSize variable dans le composant et le contrôleur sur la même valeur.

Dans la configuration du fichier Program Kestrel suivant (le projet de serveur d’une Blazor Web App ou l’API Web du serveur backend d’une Blazor WebAssembly application), le caractère générique {LIMIT} correspond à la limite en octets :

using Microsoft.AspNetCore.Http.Features;

...

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

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

Annuler un chargement de fichier

Un composant de chargement de fichiers peut détecter qu’un utilisateur a annulé un chargement à l’aide d’un CancellationToken lors d’un appel dans IBrowserFile.OpenReadStream ou StreamReader.ReadAsync.

Créez un CancellationTokenSource pour le composant InputFile. Au démarrage de la méthode OnInputFileChange, vérifiez si un chargement précédent est en cours.

Si un chargement de fichier est en cours :

Charger des fichiers côté serveur avec progression

L’exemple suivant montre comment charger des fichiers dans une application côté serveur avec affichage de la progression du chargement à l’utilisateur.

Pour utiliser l’exemple suivant dans une application de test :

  • Créez un dossier pour enregistrer les fichiers chargés pour l’environnement Development : Development/unsafe_uploads.
  • Configurez la taille de fichier maximale (maxFileSize, 15 Ko dans l’exemple suivant) et le nombre maximal de fichiers autorisés (maxAllowedFiles, 3 dans l’exemple suivant).
  • Attribuez éventuellement à la mémoire tampon une valeur différente (10 Ko dans l’exemple suivant) si vous voulez que la progression affichée offre une plus grande granularité. Nous vous déconseillons d’utiliser une mémoire tampon supérieure à 30 Ko en raison des problèmes de performances et de sécurité que cela occasionne.

Warning

L’exemple enregistre des fichiers sans analyser leur contenu, et l’aide fournie dans cet article ne prend pas en compte les autres bonnes pratiques de sécurité pour les fichiers chargés. Sur les systèmes de mise en lots et de production, désactivez l’autorisation d’exécution sur le dossier de chargement et analysez les fichiers avec une API d’analyseur antivirus/anti-programme malveillant de suite après le chargement. Pour plus d’informations, consultez Charger des fichiers dans 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;
    }
}

Pour plus d’informations, consultez les ressources d’API suivantes :

  • FileStream : fournit un Stream pour un fichier, prenant en charge les opérations de lecture et d’écriture synchrones et asynchrones.
  • FileStream.ReadAsync : le composant FileUpload3 précédent lit le flux de manière asynchrone avec ReadAsync. La lecture synchrone d’un flux avec Read n’est pas prise en charge dans les composants Razor.

Flux de fichiers

Avec l’interactivité du serveur, les données de fichier sont diffusées en flux continu via la connexion SignalR vers le code .NET sur le serveur au fur et à mesure que le fichier est lu.

RemoteBrowserFileStreamOptions permet de configurer les caractéristiques de chargement des fichiers.

Pour un composant rendu par WebAssembly, les données de fichier sont transmises directement dans le code .NET au sein du navigateur.

Charger un aperçu des images

Pour obtenir un aperçu des images en cours de chargement, commencez par ajouter un composant InputFile avec une référence de composant et un gestionnaire OnChange :

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

Ajoutez un élément image avec une référence d’élément, qui sert d’espace réservé pour l’aperçu de l’image :

<img @ref="previewImageElem" />

Ajoutez les références associées :

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

En JavaScript, ajoutez une fonction appelée avec un input HTML et un élément img qui effectue les opérations suivantes :

  • Extraction du fichier sélectionné.
  • Création d’une URL d’objet avec createObjectURL.
  • Définition d’un écouteur d’événements pour révoquer l’URL d’objet avec revokeObjectURL une fois l’image chargée, de façon à éviter une fuite de mémoire.
  • Définition de la source de l’élément img pour afficher l’image.
window.previewImage = (inputElem, imgElem) => {
  const url = URL.createObjectURL(inputElem.files[0]);
  imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
  imgElem.src = url;
}

Enfin, utilisez un IJSRuntime injecté pour ajouter le gestionnaire OnChange qui appelle la fonction JavaScript :

@inject IJSRuntime JS

...

@code {
    ...

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

L’exemple précédent concerne le chargement d’une seule image. L’approche peut être étendue afin de prendre en charge plusieurs (multiple) images.

Le composant FileUpload4 ci-dessous montre l’exemple complet.

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

Enregistrer des petits fichiers directement dans une base de données avec EF Core

De nombreuses applications ASP.NET Core utilisent Entity Framework Core (EF Core) pour gérer les opérations de base de données. L’enregistrement de miniatures et d’avatars directement dans la base de données est une exigence courante. Cette section illustre une approche générale qui peut être améliorée pour les applications de production.

Le modèle suivant :

  • Est basé sur l’application tutorielle de base de données de films Blazor.
  • Peut être amélioré avec du code supplémentaire pour le retour de validation concernant la taille de fichier et le type de contenu.
  • Entraîne une pénalité de performance et un risque DoS . Pesez soigneusement le risque lors de la lecture d’un fichier en mémoire et envisagez d’autres approches, en particulier pour les fichiers plus volumineux. Les autres approches incluent l’enregistrement de fichiers directement sur le disque ou un service tiers pour les vérifications antivirus/anti-programme malveillant, le traitement supplémentaire et la distribution aux clients.

Pour que l’exemple suivant fonctionne dans un Blazor Web App (.NET 8 ou version ultérieure), le composant doit adopter un mode de rendu interactif (par exemple @rendermode InteractiveServer) pour appeler HandleSelectedThumbnail un InputFile changement de fichier de composant (OnChange paramètre/événement). Blazor Server Les composants d’application sont toujours interactifs et ne nécessitent pas de mode de rendu.

Dans l'exemple suivant, une petite miniature (<= 100 Ko) dans un IBrowserFile est enregistrée dans une base de données avec EF Core. Si un fichier n’est pas sélectionné par l’utilisateur pour le InputFile composant, une miniature par défaut est enregistrée dans la base de données.

La miniature (default-thumbnail.jpg) par défaut se trouve à la racine du projet et le paramètre Copier dans le répertoire de sortie est Copier si plus récent :

Image miniature générique par défaut

Le Movie modèle (Movie.cs) a une propriété (Thumbnail) pour contenir les données d’image miniature :

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

Les données d’image sont stockées sous forme d’octets dans la base de données en tant que varbinary(MAX). L’application encode les octets de base 64 pour l’affichage, car les données encodées en base 64 sont environ un tiers plus grands que les octets bruts de l’image, par conséquent, les données d’image de base 64 nécessitent un stockage de base de données supplémentaire et réduisent les performances des opérations de lecture/écriture de base de données.

Les composants qui affichent la miniature transmettent les données d’image à l’attribut img de la balise src sous forme de données JPEG encodées en base 64 :

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

Dans le composant suivant Create , un chargement d’image est traité. Vous pouvez améliorer davantage l’exemple avec la validation personnalisée pour le type de fichier et la taille à l’aide des approches de la validation des formulaires ASP.NET CoreBlazor. Pour afficher le composant complet Create sans le code de chargement miniature dans l’exemple suivant, consultez l’exemple BlazorWebAppMovies d’application dans le Blazor référentiel GitHub d’exemples.

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

La même approche serait adoptée dans le Edit composant avec un mode de rendu interactif si les utilisateurs étaient autorisés à modifier l’image miniature d’un film.

Charger des fichiers dans un service externe

Au lieu d’avoir une application qui gère les octets de chargement de fichiers et le serveur de l’application qui reçoit les fichiers chargés, les clients peuvent charger directement les fichiers dans un service externe. L’application peut traiter de manière sécurisée les fichiers depuis le service externe à la demande. Cette approche endurcit l’application et son serveur face aux attaques malveillantes et aux problèmes potentiels de performances.

Envisagez une approche qui utilise Azure Files, Stockage Blob Azure ou un service tiers avec les avantages potentiels suivants :

Pour plus d’informations sur le Stockage Blob Azure et Azure Files, consultez la documentation Stockage Azure.

Limite de taille des messages SignalR côté serveur

Les chargements de fichiers peuvent échouer même avant d’avoir débuté, au moment où Blazor récupère des données sur les fichiers qui dépassent la taille de message SignalR maximale.

SignalRdéfinit une limite de taille de message qui s’applique à tous les messages reçus par Blazor, et le composant InputFile diffuse les fichiers vers le serveur dans des messages qui respectent la limite configurée. Cependant, le premier message, qui indique l’ensemble de fichiers à charger, est envoyé sous la forme d’un seul et même message. La taille du premier message peut dépasser la limite de taille de message SignalR. Le problème n’est pas lié à la taille des fichiers, mais à leur nombre.

L’erreur journalisée ressemble à l’erreur suivante :

Erreur : Connexion déconnectée avec l’erreur « Erreur : Le serveur a retourné une erreur à la fermeture : Connexion fermée avec une erreur. ». e.log @ blazor.server.js:1

Lors d’un chargement de fichiers, il est rare d’atteindre la limite de taille de message avec le premier message. Si cette limite est atteinte, l’application peut configurer HubOptions.MaximumReceiveMessageSize avec une valeur supérieure.

Pour plus d’informations sur la configuration de SignalR et sur la définition de MaximumReceiveMessageSize, consultez l’aide ASP.NET Core BlazorSignalR.

Paramètre d’invocations parallèles maximales par hub client

Blazor dépend de MaximumParallelInvocationsPerClient défini sur 1, qui est la valeur par défaut.

En augmentant la valeur, on obtient une forte probabilité que les opérations CopyTo lancent System.InvalidOperationException: 'Reading is not allowed after reader was completed.'. Pour plus d’informations, consultez MaximumParallelInvocationsPerClient > 1 interrompt le chargement de fichiers en mode Blazor Server ( dotnet/aspnetcore#53951).

Dépanner

La ligne qui appelle IBrowserFile.OpenReadStream renvoie une exception System.TimeoutException :

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

Causes possibles :

Ressources supplémentaires