Partage via


Chargements de fichiers Blazor ASP.NET Core

Remarque

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

Avertissement

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 8 de cet article.

Important

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 8 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 effectue le rendu d’un élément HTML <input> de type file pour le chargement d’un fichier unique. 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="">

Remarque

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 les données d’un fichier sélectionné par l’utilisateur, appelez IBrowserFile.OpenReadStream sur le fichier et lisez à partir du flux retourné. Pour plus d’informations, consultez la section Flux de fichiers.

OpenReadStream applique une taille maximale en octets de 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.

Si vous avez besoin d’accéder à un Stream qui représente les octets du fichier, utilisez IBrowserFile.OpenReadStream. Évitez de lire le flux de fichiers entrant directement en mémoire de façon simultanée. 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 des applications et comportent un risque d’attaque par 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 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 représente le fichier chargé et implémente IBrowserFile. Des implémentations fonctionnelles pour IBrowserFile sont présentées dans les composants de chargement de fichiers plus loin dans cet article.

Pris 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 dans le chemin fourni :

await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().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());

Non recommandée : 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()).ReadToEndAsync();

Non recommandée : l’approche suivante n’est PAS recommandée pour le stockage Blob Microsoft Azure, 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().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 d’inversion de contrôle (IoC)

Si vous utilisez le conteneur Autofac d’inversion de contrôle (IoC) au lieu du conteneur intégré d’injection de dépendances ASP.NET Core, définissez DisableImplicitFromServicesParameters sur true dans les options du hub de gestionnaire de circuit côté serveur. Pour plus d’informations, consultez FileUpload: Did not receive any data in the allotted time (dotnet/aspnetcore #38842).

Limites de taille de fichier en lecture et chargement

Côté serveur ou côté client, il n’existe aucune limite de taille de lecture ou de chargement de fichier spécifiquement pour le composant InputFile. Toutefois, Blazor côté client lit les octets du fichier dans une mémoire tampon de taille fixe (Array Buffer) JavaScript unique pendant le marshaling des données de JavaScript à C#, limité à 2 Go ou à la mémoire disponible de l’appareil. Les chargements de fichiers volumineux (> 250 Mo) peuvent échouer pour les chargements côté client utilisant le composant InputFile. Pour plus d’informations, consultez les discussions suivantes :

La taille de fichier maximale prise en charge pour le composant InputFile est de 2 Go. En outre, Blazor côté client lit les octets du fichier dans une mémoire tampon de taille fixe (Array Buffer) JavaScript unique pendant le marshaling des données de JavaScript à C#, limité à 2 Go ou à la mémoire disponible de l’appareil. Les chargements de fichiers volumineux (> 250 Mo) peuvent échouer pour les chargements côté client utilisant le composant InputFile. Pour plus d’informations, consultez les discussions suivantes :

Pour les chargements de fichiers côté client volumineux qui échouent lors de la tentative d’utilisation du composant InputFile, nous vous recommandons de segmenter les fichiers volumineux avec un composant personnalisé à l’aide de plusieurs requêtes de plage HTTP au lieu d’utiliser le composant InputFile.

Des travaux sont actuellement planifiés pour .NET 9 (fin 2024) afin de répondre à la limitation du chargement de taille de fichier côté client.

Examples

Les exemples suivants illustrent le chargement de plusieurs fichiers dans un composant. InputFileChangeEventArgs.GetMultipleFiles permet la lecture de 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.File permet la lecture du seul et unique fichier si le chargement de fichier 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 contenus dans le fichier _Imports.razor de l’application. Quand l’espace de noms est présent dans le fichier _Imports.razor, les membres de l’API ont accès 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 fichier côté serveur

Pour utiliser le code suivant, créez un Development/unsafe_uploadsdossier à 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 = 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
            {
                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 fichier côté client

L’exemple suivant traite les octets de fichiers et n’envoie pas de fichiers vers une destination extérieure à 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 d’affichage WebAssembly interactif (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 = 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;
    }
}
@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 retourne les métadonnées exposées par le navigateur sous forme de propriétés. Utilisez ces métadonnées pour la validation préliminaire.

Ne vous fiez jamais aux valeurs des propriétés précédentes, en particulier celles de la propriété Name, pour l’affichage dans l’interface utilisateur. Considérez 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.

Charger des fichiers sur un serveur avec rendu côté serveur

Cette section s’applique aux composants de serveur interactif dans les Blazor Web App ou les applications Blazor Server.

L’exemple suivant illustre le chargement de fichiers d’une application côté serveur sur un contrôleur d’API web back-end 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 façon différée juste avant que les premiers octets du flux soient demandés. Le flux n’est pas transmis du navigateur au serveur tant que la lecture du flux ne commence pas 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 = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);

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

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);
                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);
                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);
                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string Name { get; set; }
    }
}

Si le composant limite les chargements de fichiers à un seul fichier à la fois ou s’il adopte uniquement le rendu interactif côté client (RSC, InteractiveWebAssembly), le composant peut éviter l’utilisation du LazyBrowserFileStream et utiliser un Stream. Voici 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, le composant peut éviter l’utilisation du LazyBrowserFileStream et utiliser un Stream. Voici 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.

Important

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 XSRF/CSRF (Cross-Site Request Forgery) si les utilisateurs de chargement de fichiers sont authentifiés.

Remarque

La liaison de valeurs de formulaire avec l’attribut [FromForm] n’est pas prise en charge en mode natif 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 de valeurs de formulaire avec les API minimales est disponible dans ASP.NET Core dans .NET 7 ou 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 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.

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é. N’approuvez jamais le nom de fichier fourni par le navigateur, car un attaquant peut choisir un nom de fichier existant qui remplace un fichier existant, ou envoyer un chemin qui tente d’écrire en dehors de l’application.

L’application serveur doit inscrire 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 (RSC)

Cette section s’applique aux composants utilisant le rendu côté client dans les Blazor Web App ou les applications Blazor WebAssembly.

L’exemple suivant illustre le chargement de fichiers vers un contrôleur d’API web dans une application séparée, éventuellement sur un serveur séparé, à partir d’un composant dans une Blazor Web App qui adopte le rendu côté client ou d’un composant dans une application Blazor WebAssembly.

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

Remarque

La classe UploadResult précédente peut être partagée entre les projets basés client et serveur. Lorsque les projets du client et du serveur partagent la classe, ajoutez une importation aux fichiers _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.

Avertissement

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 principal Blazor Web App, ajoutez IHttpClientFactory et les services associés dans le fichier Program du projet :

builder.Services.AddHttpClient();

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

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

Le projet de client (.Client) d’une Blazor Web App doit également inscrire un HttpClient pour les requêtes HTTP POST auprès d’un contrôleur d’API web back-end. Validez ou ajoutez ce qui suit au fichier Program du projet de 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). Cette propriété récupère l’adresse de base de l’application. Elle est généralement dérivée de la valeur href de la balise <base> 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.

Spécifiez l’attribut de mode de rendu WebAssembly interactif en haut du composant suivant dans une Blazor Web App :

@rendermode InteractiveWebAssembly

FileUpload2.razor :

@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 = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

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

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

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

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

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

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

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

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

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

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

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

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            uploadResults = uploadResults.Concat(newUploadResults).ToList();
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName);

        if (result is null)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result = new();
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string Name { get; set; }
    }
}

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

Remarque

La liaison de valeurs de formulaire avec l’attribut [FromForm] n’est pas prise en charge en mode natif 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 de valeurs de formulaire avec les API minimales est disponible dans ASP.NET Core dans .NET 7 ou 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, mettez à jour l’espace de noms du projet partagé pour qu’il corresponde au projet partagé si un projet partagé fournit la classe UploadResult.

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;
using BlazorSample.Shared;

[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é. N’approuvez jamais le nom de fichier fourni par le navigateur, car un attaquant peut choisir un nom de fichier existant qui remplace un fichier existant, ou envoyer un chemin qui tente d’écrire en dehors de l’application.

L’application serveur doit inscrire 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.

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 la progression du chargement présentée à 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 = 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);

                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 :

  • Stream : Fournit un FileStream pour un fichier, prenant en charge les opérations de lecture et d’écriture à la fois 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é serveur, les données de fichier sont transmises en continu au code .NET sur le serveur via la connexion SignalR au fur et à mesure que le fichier est lu.

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

Pour un composant du rendu WebAssembly, les données de fichier sont transmises directement au code .NET dans le 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);
}

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.

SignalR définit une limite de taille de message qui s’applique à chaque message que reçoit Blazor, et le composant InputFile transmet les fichiers au 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.

Nombre maximal d’appels parallèles par paramètre de hub client

Blazor s’appuie sur MaximumParallelInvocationsPerClient défini sur 1, qui est la valeur par défaut.

L’augmentation de la valeur entraîne une probabilité élevée que CopyTo les opérations lèvent 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).

Résolution des problèmes

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