Gérer les erreurs dans les applications ASP.NET Core Blazor

Remarque

Ceci n’est pas la dernière version de cet article. 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 décrit comment Blazor gère les exceptions non prises en charge et comment développer des applications qui détectent et gèrent les erreurs.

Erreurs détaillées pendant le développement

Lorsqu’une application Blazor ne fonctionne pas correctement pendant le développement, la réception d’informations détaillées sur les erreurs de l’application aide à diagnostiquer et résoudre le problème. Lorsqu’une erreur se produit, les applications Blazor affichent une barre jaune clair en bas de l’écran :

  • Pendant le développement, la barre vous dirige vers la console du navigateur, où vous pouvez voir l’exception.
  • En production, la barre avertit l’utilisateur qu’une erreur s’est produite et recommande d’actualiser le navigateur.

L’interface utilisateur de cette expérience de gestion des erreurs fait partie des modèles de projet Blazor. Les versions des modèles de projet Blazor n’utilisent pas toutes l’attribut data-nosnippet pour signaler aux navigateurs de ne pas mettre en cache le contenu de l’interface utilisateur des erreurs, mais toutes les versions de la documentation Blazor appliquent l’attribut.

Dans une application web Blazor , personnalisez l’expérience dans le composant MainLayout . Étant donné que le Assistance de balise d’environnement (par exemple, <environment include="Production">...</environment>) n’est pas pris en charge dans les composants Razor , l’exemple suivant injecte IHostEnvironment pour configurer des messages d’erreur pour différents environnements.

En haut de MainLayout.razor :

@inject IHostEnvironment HostEnvironment

Créez ou modifiez le balisage de l’interface utilisateur d’erreur Blazor :

<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Dans une application Blazor Server, personnalisez l’expérience dans le fichier Pages/_Host.cshtml. L’exemple suivant utilise l' Assistance de balise d’environnement pour configurer les messages d’erreur pour différents environnements.

Dans une application Blazor Server, personnalisez l’expérience dans le fichier Pages/_Layout.cshtml. L’exemple suivant utilise l' Assistance de balise d’environnement pour configurer les messages d’erreur pour différents environnements.

Dans une application Blazor Server, personnalisez l’expérience dans le fichier Pages/_Host.cshtml. L’exemple suivant utilise l' Assistance de balise d’environnement pour configurer les messages d’erreur pour différents environnements.

Créez ou modifiez le balisage de l’interface utilisateur d’erreur Blazor :

<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Dans une application Blazor WebAssembly, personnalisez l’expérience dans le fichier wwwroot/index.html :

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

L’élément blazor-error-ui est normalement masqué en raison de la présence du style display: none de la classe CSS blazor-error-ui dans la feuille de style générée automatiquement de l’application. Lorsqu’une erreur se produit, le framework applique display: block à l’élément.

L’élément blazor-error-ui est normalement masqué en raison de la présence du style display: none de la classe CSS blazor-error-ui dans la feuille de style du site dans le dossier wwwroot/css. Lorsqu’une erreur se produit, le framework applique display: block à l’élément.

Erreurs de circuit détaillées

Cette section s’applique à Blazor Web Apps fonctionnant sur un circuit.

Cette section s’applique aux applications Blazor Server.

Les erreurs côté client n’incluent pas la pile des appels et ne fournissent pas de détails sur la cause de l’erreur, mais les journaux du serveur contiennent de telles informations. À des fins de développement, des informations d’erreur de circuit sensibles peuvent être mises à la disposition du client en activant les erreurs détaillées.

Affectez la valeur CircuitOptions.DetailedErrors à true. Pour plus d’informations et un exemple, consultez le Guide pour ASP.NET Core BlazorSignalR.

Une alternative à la définition de CircuitOptions.DetailedErrors consiste à définir la clé de configuration DetailedErrors sur true dans le fichier des paramètres d’environnement de Development de l’application (appsettings.Development.json). En outre, définissez la journalisation côté serveur SignalR (Microsoft.AspNetCore.SignalR) sur Déboguer ou Trace pour une journalisation détaillée SignalR.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

La clé de configuration DetailedErrors peut également être définie sur true à l’aide de la variable d’environnement ASPNETCORE_DETAILEDERRORS avec une valeur de true sur des serveurs d’environnement de Development/Staging ou sur votre système local.

Avertissement

Évitez toujours d’exposer des informations d’erreur aux clients sur Internet, ce qui constitue un risque de sécurité.

Erreurs détaillées pour le rendu côté serveur du composant Razor

Cette section s’applique aux applications web Blazor.

Utilisez l’option RazorComponentsServiceOptions.DetailedErrors pour contrôler la production d’informations détaillées sur les erreurs pour le rendu côté serveur du composant Razor. La valeur par défaut est false.

L’exemple suivant active les erreurs détaillées :

builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());

Avertissement

Activez uniquement les erreurs détaillées dans l’environnement de Development. Des erreurs détaillées peuvent contenir des informations sensibles sur l’application que les utilisateurs malveillants peuvent utiliser dans une attaque.

L’exemple précédent fournit un degré de sécurité en définissant la valeur de DetailedErrors en fonction de la valeur retournée par IsDevelopment. Lorsque l’application se trouve dans l’environnement Development , DetailedErrors est définie sur true. Cette approche n’est pas infaillible, car il est possible d’héberger une application de production sur un serveur public dans l’environnement Development.

Gérer les exceptions non prises en charge dans le code du développeur

Pour qu’une application continue après une erreur, elle doit avoir une logique de gestion des erreurs. Les sections ultérieures de cet article décrivent les sources potentielles d’exceptions non prises en charge.

En production, ne restituez pas les messages d’exception de framework ou les traces de pile dans l’interface utilisateur. Le rendu des messages d’exception ou des traces de pile peut :

  • Divulguer des informations sensibles aux utilisateurs finaux.
  • Aider un utilisateur malveillant à découvrir les faiblesses d’une application qui peuvent compromettre la sécurité de l’application, du serveur ou du réseau.

Exceptions non gérées pour les circuits

Cette section s’applique aux applications Web côté serveur fonctionnant sur un circuit.

Razor composants avec l’interactivité du serveur activée sont avec état sur le serveur. Bien que les utilisateurs interagissent avec le composant sur le serveur, ils conservent une connexion au serveur appelé circuit. Le circuit contient des instances de composants actifs, ainsi que de nombreux autres aspects de l’état, comme :

  • La sortie rendue la plus récente des composants.
  • L’ensemble actuel de délégués de gestion des événements qui peuvent être déclenchés par des événements côté client.

Si un utilisateur ouvre l’application dans plusieurs onglets de navigateur, il crée plusieurs circuits indépendants.

Blazor traite la plupart des exceptions non prises en charge comme irrécupérables pour le circuit où elles se produisent. Si un circuit est arrêté en raison d’une exception non prise en charge, l’utilisateur peut uniquement continuer à interagir avec l’application en rechargeant la page pour créer un nouveau circuit. Les circuits en dehors de celui qui est terminé, qui sont des circuits pour d’autres utilisateurs ou d’autres onglets de navigateur, ne sont pas affectés. Ce scénario est similaire à une application de bureau qui se bloque. L’application plantée doit être redémarrée, mais les autres applications ne sont pas affectées.

Le framework met fin à un circuit lorsqu’une exception non prise en charge se produit pour les raisons suivantes :

  • Une exception non prise en charge laisse souvent le circuit dans un état non défini.
  • Le fonctionnement normal de l’application ne peut pas être garanti après une exception non prise en charge.
  • Des vulnérabilités de sécurité peuvent apparaître dans l’application si le circuit continue dans un état non défini.

Gestion globale des exceptions

Pour la gestion globale des exceptions, consultez les sections suivantes :

Limites d’erreur

Les limites d’erreur fournissent une approche pratique pour la gestion des exceptions. Le composant ErrorBoundary :

  • Affiche son contenu enfant lorsqu’une erreur ne s’est pas produite.
  • Affiche l’interface utilisateur d’erreur lorsqu’une exception non prise en charge est levée.

Pour définir une limite d’erreur, utilisez le composant ErrorBoundary pour encapsuler le contenu existant. L’application continue de fonctionner normalement, mais la limite d’erreur gère les exceptions non prises en charge.

<ErrorBoundary>
    ...
</ErrorBoundary>

Pour implémenter une limite d’erreur de manière globale, ajoutez la limite autour du contenu du corps du layout principal de l’application.

Dans MainLayout.razor :

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

Dans Blazor Web Apps avec la limite d’erreur appliquée uniquement à un composant MainLayout statique, la limite est active uniquement pendant la phase de rendu statique côté serveur (SSR statique). La limite ne s’active pas juste parce qu’un composant plus bas dans la hiérarchie des composants est interactif. Pour activer l’interactivité à grande échelle pour le composant MainLayout et le reste des composants situés plus bas dans la hiérarchie des composants, activez le rendu interactif pour les instances de composant HeadOutlet et Routes dans le composant App (Components/App.razor). L’exemple suivant adopte le mode de rendu Serveur interactif (InteractiveServer) :

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Si vous préférez ne pas activer l’interactivité du serveur sur l’ensemble de l’application depuis le composant Routes, placez la limite d’erreur plus bas dans la hiérarchie des composants. Par exemple, placez la limite d’erreur autour du balisage dans les composants individuels qui activent l’interactivité, et non dans le layout principal de l’application. Les concepts importants à garder à l’esprit sont que chaque fois que la limite d’erreur est placée :

  • Si la limite d’erreur n’est pas interactive, elle ne peut s’activer sur le serveur que pendant le rendu statique. Par exemple, la limite peut s’activer lorsqu’une erreur est envoyée dans une méthode de cycle de vie des composants.
  • Si la limite d’erreur est interactive, elle peut s’activer pour les composants de rendu Serveur interactif inclus.

Prenons l’exemple suivant, où le composant Counter lève une exception si le nombre est incrémenté au-delà de cinq.

Dans Counter.razor:

private void IncrementCount()
{
    currentCount++;

    if (currentCount > 5)
    {
        throw new InvalidOperationException("Current count is too big!");
    }
}

Si l’exception non prises en charge est levée pour un currentCount de plus de cinq :

  • L’erreur est enregistrée normalement (System.InvalidOperationException: Current count is too big!).
  • L’exception est gérée par la limite d’erreur.
  • L’interface utilisateur d’erreur est affichée par la limite d’erreur avec le message d’erreur par défaut suivant : An error has occurred.

Par défaut, le composant ErrorBoundary restitue un élément <div> vide avec la classe CSS blazor-error-boundary pour son contenu d’erreur. Les couleurs, le texte et l’icône de l’interface utilisateur par défaut sont définis à l’aide de CSS dans la feuille de style de l’application dans le dossier wwwroot. Vous êtes donc libre de personnaliser l’interface utilisateur d’erreur.

Modifiez le contenu d’erreur par défaut en définissant la propriété ErrorContent :

<ErrorBoundary>
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

Étant donné que la limite d’erreur est définie dans la disposition dans les exemples précédents, l’interface utilisateur d’erreur est visible, quelle que soit la page vers laquelle l’utilisateur accède après l’erreur. Dans la plupart des scénarios, nous vous recommandons de définir étroitement les limites d’erreur. Si vous limitez largement une limite d’erreur, vous pouvez la réinitialiser à un état non d’erreur sur les événements de navigation de page suivants en appelant la méthode Recover de la limite d’erreur.

Dans MainLayout.razor:

...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

Pour éviter la boucle infinie où la récupération revient simplement à réactiver un composant qui lève à nouveau l’erreur, n’appelez pas Recover de la logique de rendu. Appelez uniquement Recover lorsque :

  • L’utilisateur effectue un mouvement d’interface utilisateur, par exemple en sélectionnant un bouton pour indiquer qu’il souhaite réessayer une procédure ou lorsque l’utilisateur accède à un nouveau composant.
  • Une logique supplémentaire efface également l’exception. Lorsque le composant est remangé, l’erreur ne se reproduit pas.

Gestion globale des exceptions alternative

Une alternative à l’utilisation des limites d’erreur (ErrorBoundary) consiste à passer un composant d’erreur personnalisé en tant que CascadingValue aux composants enfants. L’un des avantages de l’utilisation d’un composant par rapport à l’utilisation d’un service injecté ou d’une implémentation d’enregistreur d’événements personnalisé est qu’un composant en cascade peut restituer du contenu et appliquer des styles CSS en cas d’erreur.

L’exemple de composant Error suivant journalise simplement les erreurs, mais les méthodes du composant peuvent traiter les erreurs de n’importe quelle façon requise par l’application, notamment en utilisant plusieurs méthodes de traitement des erreurs.

Error.razor:

@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if ProcessError directly participates in 
        // rendering. If ProcessError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}

Remarque

Pour plus d’informations sur RenderFragment, consultez Composants ASP.NET Core Razor.

Dans le composant Routes, encapsulez le composant Router (<Router>...</Router>) avec le composant Error. Cela permet au composant Error de descendre en cascade vers n’importe quel composant de l’application où le composant Error est reçu en tant que CascadingParameter.

Dans Routes.razor :

<Error>
    <Router ...>
        ...
    </Router>
</Error>

Dans le composant App, encapsulez le composant Router (<Router>...</Router>) avec le composant Error. Cela permet au composant Error de descendre en cascade vers n’importe quel composant de l’application où le composant Error est reçu en tant que CascadingParameter.

Dans App.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

Pour traiter les erreurs dans un composant :

  • Désignez le composant Error en tant que CascadingParameter dans le bloc @code. Dans un exemple de composant Counter dans une application basée sur un modèle de projet Blazor, ajoutez la propriété Error suivante :

    [CascadingParameter]
    public Error? Error { get; set; }
    
  • Appelez une méthode de traitement des erreurs dans n’importe quel bloc catch avec un type d’exception approprié. L’exemple de composant Error n’offre qu’une seule méthode ProcessError, mais le composant de traitement des erreurs peut fournir un nombre quelconque de méthodes de traitement des erreurs pour répondre à d’autres exigences de traitement des erreurs dans l’application. Dans l’exemple de composant Counter suivant, une exception est levée et interceptée lorsque le nombre est supérieur à cinq :

    @code {
        private int currentCount = 0;
    
        [CascadingParameter]
        public Error? Error { get; set; }
    
        private void IncrementCount()
        {
            try
            {
                currentCount++;
    
                if (currentCount > 5)
                {
                    throw new InvalidOperationException("Current count is over five!");
                }
            }
            catch (Exception ex)
            {
                Error?.ProcessError(ex);
            }
        }
    }
    

À l’aide du composant Error précédent avec les modifications apportées à un composant Counter, la console des outils de développement du navigateur indique l’erreur interceptée et journalisée :

fail: {COMPONENT NAMESPACE}.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

Si la méthode ProcessError participe directement au rendu, comme l’affichage d’une barre de messages d’erreur personnalisée ou la modification des styles CSS des éléments rendus, appelez StateHasChanged à la fin de la méthode ProcessErrors pour réactiver l’interface utilisateur.

Étant donné que les approches de cette section gèrent les erreurs avec une instruction try-catch, la connexion d’une application SignalR entre le client et le serveur n’est pas interrompue lorsqu’une erreur se produit, et le circuit reste actif. Les autres exceptions non prises en charge restent irrécupérables pour un circuit. Pour plus d’informations, consultez la section sur comment un circuit réagit aux exceptions non gérées.

Une application peut utiliser un composant de traitement des erreurs comme valeur en cascade pour traiter les erreurs de manière centralisée.

Le composant Error suivant se transmet lui-même en tant que CascadingValue aux composants enfants. L’exemple journalise simplement l’erreur, mais les méthodes du composant peuvent traiter les erreurs de n’importe quelle façon requise par l’application, notamment en utilisant plusieurs méthodes de traitement des erreurs. L’un des avantages de l’utilisation d’un composant par rapport à l’utilisation d’un service injecté ou d’une implémentation d’enregistreur d’événements personnalisé est qu’un composant en cascade peut restituer du contenu et appliquer des styles CSS en cas d’erreur.

Error.razor:

@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}

Remarque

Pour plus d’informations sur RenderFragment, consultez Composants ASP.NET Core Razor.

Dans le composant App, encapsulez le composant Router avec le composant Error. Cela permet au composant Error de descendre en cascade vers n’importe quel composant de l’application où le composant Error est reçu en tant que CascadingParameter.

App.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

Pour traiter les erreurs dans un composant :

  • Désignez le composant Error en tant que CascadingParameter dans le bloc @code :

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Appelez une méthode de traitement des erreurs dans n’importe quel bloc catch avec un type d’exception approprié. L’exemple de composant Error n’offre qu’une seule méthode ProcessError, mais le composant de traitement des erreurs peut fournir un nombre quelconque de méthodes de traitement des erreurs pour répondre à d’autres exigences de traitement des erreurs dans l’application.

    try
    {
        ...
    }
    catch (Exception ex)
    {
        Error.ProcessError(ex);
    }
    

À l’aide de l’exemple précédent de composant Error et de la ProcessError méthode, la console des outils de développement du navigateur indique l’erreur interceptée et journalisée :

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: La référence d’objet n’est pas définie sur une instance d’un objet.

Si la méthode ProcessError participe directement au rendu, comme l’affichage d’une barre de messages d’erreur personnalisée ou la modification des styles CSS des éléments rendus, appelez StateHasChanged à la fin de la méthode ProcessErrors pour réactiver l’interface utilisateur.

Étant donné que les approches de cette section gèrent les erreurs avec une instruction try-catch, la connexion SignalR d’une application Blazor entre le client et le serveur n’est pas interrompue lorsqu’une erreur se produit, et le circuit reste actif. Toute exception non prise en charge est irrécupérable pour un circuit. Pour plus d’informations, consultez la section sur comment un circuit réagit aux exceptions non gérées.

Erreurs de journalisation avec un fournisseur persistant

Si une exception non gérée se produit, l’exception est enregistrée dans les instances ILogger configurées dans le conteneur de service. Par défaut, les applications Blazor consignent dans la sortie de la console avec le fournisseur de journalisation de console. Envisagez de vous connecter à un emplacement sur le serveur (ou l’API web principale pour les applications côté client) avec un fournisseur qui gère la taille des journaux et la rotation des journaux. L’application peut également utiliser un service de gestion des performances des applications (APM), comme Azure Application Insights (Azure Monitor).

Remarque

Les fonctionnalités de Application Insights natives pour prendre en charge les applications côté client et la prise en charge native de Blazor framework pour Google Analytics peuvent devenir disponibles dans les futures versions de ces technologies. Pour plus d’informations, consultez Prise en charge d’App Insights dans Blazor WASM côté client (microsoft/ApplicationInsights-dotnet #2143) et Analyse web et diagnostics (comprend des liens vers des implémentations de la communauté) (dotnet/aspnetcore #5461). En attendant, une application côté client peut utiliser le SDK JavaScript Application Insights avec JSinteropérabilité pour consigner des erreurs directement dans Application Insights à partir d’une application côté client.

Pendant le développement dans une application Blazor fonctionnant sur un circuit, l’application envoie généralement les détails complets des exceptions à la console du navigateur pour faciliter le débogage. En production, les erreurs détaillées ne sont pas envoyées aux clients, mais les détails complets d’une exception sont enregistrés sur le serveur.

Vous devez déterminer quels incidents journaliser et le niveau de gravité des incidents journalisés. Des utilisateurs hostiles pourraient être en mesure de déclencher des erreurs délibérément. Par exemple, ne journalisez pas d’incident à partir d’une erreur où un ProductId inconnu est fourni dans l’URL d’un composant qui affiche les détails du produit. Toutes les erreurs ne doivent pas être traitées comme des incidents pour la journalisation.

Pour plus d’informations, consultez les articles suivants :

‡S’applique aux applications Blazor côté serveur et à d’autres applications ASP.NET Core côté serveur qui sont des applications back-end d’API web pour Blazor. Les applications côté client peuvent intercepter et envoyer des informations d’erreur sur le client à une API web, qui enregistre les informations d’erreur sur un fournisseur de journalisation persistant.

Si une exception non gérée se produit, l’exception est enregistrée dans les instances ILogger configurées dans le conteneur de service. Par défaut, les applications Blazor consignent dans la sortie de la console avec le fournisseur de journalisation de console. Envisagez de journaliser vers un emplacement plus permanent sur le serveur en envoyant des informations d’erreur à une API web back-end qui utilise un fournisseur de journalisation avec la gestion de la taille des journaux et la rotation des journaux. L’application d’API web back-end peut également utiliser un service de gestion des performances des applications (APM), comme Azure Application Insights (Azure Monitor)†, pour enregistrer les informations d’erreur qu’elle reçoit des clients.

Vous devez déterminer quels incidents journaliser et le niveau de gravité des incidents journalisés. Des utilisateurs hostiles pourraient être en mesure de déclencher des erreurs délibérément. Par exemple, ne journalisez pas d’incident à partir d’une erreur où un ProductId inconnu est fourni dans l’URL d’un composant qui affiche les détails du produit. Toutes les erreurs ne doivent pas être traitées comme des incidents pour la journalisation.

Pour plus d’informations, consultez les articles suivants :

†Les fonctionnalités de Application Insights natives pour prendre en charge les applications côté client et la prise en charge native de Blazor framework pour Google Analytics peuvent devenir disponibles dans les futures versions de ces technologies. Pour plus d’informations, consultez Prise en charge d’App Insights dans Blazor WASM côté client (microsoft/ApplicationInsights-dotnet #2143) et Analyse web et diagnostics (comprend des liens vers des implémentations de la communauté) (dotnet/aspnetcore #5461). En attendant, une application côté client peut utiliser le SDK JavaScript Application Insights avec JSinteropérabilité pour consigner des erreurs directement dans Application Insights à partir d’une application côté client.

‡S’applique aux applications ASP.NET Core côté serveur qui sont des applications back-end d’API web pour les applications Blazor. Les applications côté client interceptent et envoient des informations d’erreur à une API web, qui consigne les informations d’erreur à un fournisseur de journalisation persistant.

Emplacements où des erreurs peuvent se produire

Le code de framework et d’application peut déclencher des exceptions non prises en charge dans l’un des emplacements suivants, qui sont décrits plus en détail dans les sections suivantes de cet article :

Instanciation de composant

Quand Blazor crée une instance d’un composant :

  • Le constructeur du composant est appelé.
  • Les constructeurs des services de DI fournis au constructeur du composant via la directive @inject ou l’attribut [Inject] sont appelés.

Une erreur dans un constructeur ou un setter exécuté pour toute propriété [Inject] entraîne une exception non prise en charge et empêche le framework d’instancier le composant. Si l’application fonctionne sur un circuit, le circuit échoue. Si la logique du constructeur peut lever des exceptions, l’application doit intercepter les exceptions à l’aide d’une instruction try-catch avec gestion des erreurs et journalisation.

Méthodes de cycle de vie

Pendant la durée de vie d’un composant, Blazor appelle des méthodes de cycle de vie. Si une méthode de cycle de vie lève une exception, de manière synchrone ou asynchrone, l’exception est irrécupérable pour un circuit. Pour que les composants traitent les erreurs dans les méthodes de cycle de vie, ajoutez une logique de gestion des erreurs.

Dans l’exemple suivant, où OnParametersSetAsync appelle une méthode pour obtenir un produit :

  • Une exception levée dans la méthode ProductRepository.GetProductByIdAsync est gérée par une instruction try-catch.
  • Lorsque le bloc catch est exécuté :
    • loadFailed est défini sur true, qui est utilisé pour afficher un message d’erreur à l’utilisateur.
    • L’erreur est journalisée.
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}

Logique de rendu

Le balisage déclaratif dans un fichier de composant Razor (.razor) est compilé dans une méthode C# appelée BuildRenderTree. Lorsqu’un composant effectue un rendu, BuildRenderTree exécute et génère une structure de données décrivant les éléments, le texte et les composants enfants du composant rendu.

La logique de rendu peut lever une exception. Un exemple de ce scénario se produit lorsque @someObject.PropertyName est évalué, mais que @someObject est null. Pour Blazor applications fonctionnant sur un circuit, une exception non gérée levée par la logique de rendu est irrécupérable pour le circuit de l’application.

Pour empêcher une NullReferenceException dans une logique de rendu, recherchez un objet null avant d’accéder à ses membres. Dans l’exemple suivant, les propriétés person.Address ne sont pas accessibles si person.Address est null :

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

Le code précédent suppose que person n’est pas null. Souvent, la structure du code garantit qu’un objet existe au moment où le composant est rendu. Dans ces cas, il n’est pas nécessaire de vérifier null dans la logique de rendu. Dans l’exemple précédent, l’existence de person peut être garantie, car person est créé lorsque le composant est instancié, comme l’illustre l’exemple suivant :

@code {
    private Person person = new();

    ...
}

Gestionnaires d’événements

Le code côté client déclenche des appels de code C# lorsque des gestionnaires d’événements sont créés avec :

  • @onclick
  • @onchange
  • D’autres attributs @on...
  • @bind

Le code du gestionnaire d’événements peut lever une exception non prise en charge dans ces scénarios.

Si l’application appelle du code susceptible d’échouer pour des raisons externes, interceptez les exceptions à l’aide d’une instruction try-catch avec gestion et journalisation des erreurs.

Si un gestionnaire d’événements lève une exception non prise en charge (par exemple, si une requête de base de données échoue) qui n’est pas interceptée et gérée par le code du développeur :

  • Le framework consigne l’exception.
  • Dans une application Blazor fonctionnant sur un circuit, l’exception est irrécupérable pour le circuit de l’application.

Mise au rebut des supports

Un composant peut être supprimé de l’interface utilisateur, par exemple parce que l’utilisateur a accédé à une autre page. Lorsqu’un composant qui implémente System.IDisposable est supprimé de l’interface utilisateur, le framework appelle la méthode Dispose du composant.

Si la méthode Dispose du composant lève une exception non gérée dans une application Blazor fonctionnant sur un circuit, l’exception est irrécupérable pour le circuit de l’application.

Si la logique de suppression peut lever des exceptions, l’application doit intercepter les exceptions à l’aide d’une instruction try-catch avec gestion des erreurs et journalisation.

Pour plus d’informations sur la suppression de composants, consultez Cycle de vie des composants ASP.NET Core Razor.

Interopérabilité JavaScript

IJSRuntime est inscrit par le framework Blazor. IJSRuntime.InvokeAsync permet au code .NET d’effectuer des appels asynchrones au runtime JavaScript (JS) dans le navigateur de l’utilisateur.

Les conditions suivantes s’appliquent à la gestion des erreurs avec InvokeAsync :

  • Si un appel à InvokeAsync échoue de façon synchrone, une exception .NET se produit. Un appel à InvokeAsync peut échouer, par exemple si les arguments fournis ne peuvent pas être sérialisés. Le code du développeur doit intercepter l’exception. Si le code d’application d’un gestionnaire d’événements ou d’une méthode de cycle de vie de composant ne gère pas une exception dans une application Blazor fonctionnant sur un circuit, l’exception résultante est irrécupérable pour le circuit de l’application.
  • Si un appel à InvokeAsync échoue de façon asynchrone, la Task .NET échoue. Un appel à InvokeAsync peut échouer, par exemple, car le code côté JS lève une exception ou retourne une Promise qui s’est terminée en tant que rejected. Le code du développeur doit intercepter l’exception. Si vous utilisez l’opérateur await, envisagez d’encapsuler l’appel de méthode dans une instruction try-catch avec la gestion des erreurs et la journalisation. Dans le cas contraire, dans une application Blazor fonctionnant sur un circuit, le code défaillant entraîne une exception non gérée irrécupérable pour le circuit de l’application.
  • Par défaut, les appels à InvokeAsync doivent se terminer dans un certain délai ou si l’appel expire. La période d’expiration par défaut est d’une minute. Le délai d’expiration protège le code contre la perte de connectivité réseau ou le code JS qui ne renvoie jamais de message d’achèvement. Si l’appel expire, la System.Threading.Tasks résultante échoue avec une OperationCanceledException. Interceptez et traitez l’exception avec journalisation.

De même, le code JS peut lancer des appels aux méthodes .NET indiquées par l’attribut [JSInvokable]. Si ces méthodes .NET lèvent une exception non prise en charge :

  • Dans une application Blazor fonctionnant sur un circuit, l’exception n’est pas considérée comme irrécupérable pour le circuit de l’application.
  • La Promise côté JS est rejetée.

Vous avez la possibilité d’utiliser le code de gestion des erreurs côté .NET ou côté JS de l’appel de méthode.

Pour plus d’informations, consultez les articles suivants :

Prérendu

Razor composants sont pré-affichés par défaut afin que leur balisage HTML rendu soit retourné dans le cadre de la requête HTTP initiale de l’utilisateur.

Dans une application Blazor fonctionnant sur un circuit, le pré-affichage fonctionne par :

  • Création d’un nouveau circuit pour tous les composants prérendus qui font partie de la même page.
  • Génération du HTML initial.
  • Traitement du circuit comme disconnected jusqu’à ce que le navigateur de l’utilisateur établisse une connexion SignalR au même serveur. Une fois la connexion établie, l’interactivité sur le circuit reprend et le balisage HTML des composants est mis à jour.

Pour les composants côté client pré-affichés, le pré-affichage fonctionne par :

  • Générant le code HTML initial sur le serveur pour tous les composants prérendus qui font partie de la même page.
  • Rendant le composant interactif sur le client une fois que le navigateur a chargé le code compilé de l’application et le runtime .NET (s’il n’est pas déjà chargé) en arrière-plan.

Si un composant lève une exception non prise en charge pendant le prérendu, par exemple, au cours d’une méthode de cycle de vie ou dans une logique de rendu :

  • Dans une application Blazor fonctionnant sur un circuit, l’exception est irrécupérable pour le circuit. Pour les composants côté client prédéfinis, l’exception empêche le rendu du composant.
  • L’exception est levée dans la pile des appels à partir du ComponentTagHelper.

Dans des circonstances normales où le prérendu échoue, la poursuite de la génération et du rendu du composant n’a pas de sens, car un composant de travail ne peut pas être rendu.

Pour tolérer les erreurs qui peuvent se produire pendant le prérendu, la logique de gestion des erreurs doit être placée à l’intérieur d’un composant qui peut lever des exceptions. Utilisez des instructions try-catch avec la gestion et la journalisation des erreurs. Au lieu d’encapsuler le ComponentTagHelper dans une instruction try-catch, placez la logique de gestion des erreurs dans le composant rendu par le ComponentTagHelper.

Scénarios avancés

Rendu récursif

Les composants peuvent être imbriqués récursivement. Cela est utile pour représenter des structures de données récursives. Par exemple, un composant TreeNode peut afficher davantage de composants TreeNode pour chacun des enfants du nœud.

Lors d’un rendu récursif, évitez de coder des modèles qui entraînent une récursivité infinie :

  • Ne restituez pas de manière récursive une structure de données qui contient un cycle. Par exemple, ne restituez pas un nœud d’arborescence dont les enfants s’incluent eux-mêmes.
  • Ne créez pas de chaîne de dispositions qui contiennent un cycle. Par exemple, ne créez pas de dispositions dont la disposition est elle-même.
  • N’autorisez pas un utilisateur final à enfreindre les invariants de récursion (règles) par le biais d’une entrée de données malveillante ou d’appels d’interopérabilité JavaScript.

Les boucles infinies pendant le rendu :

  • Entraînent la poursuite indéfinie du processus de rendu.
  • Équivalent à créer une boucle non déterminée.

Dans ces scénarios, le Blazor échoue et tente généralement de :

  • Consommer autant de temps processeur que le système d’exploitation le permet, indéfiniment.
  • Consommer une quantité illimitée de mémoire. La consommation de mémoire illimitée équivaut au scénario où une boucle non déterminée ajoute des entrées à une collection à chaque itération.

Pour éviter les cas de récursivité infinie, assurez-vous que le code de rendu récursif contient des conditions d’arrêt appropriées.

Logique d’arborescence de rendu personnalisée

La plupart des composants Razor sont implémentés en tant que fichiers de composants Razor (.razor) et sont compilés par l’infrastructure pour produire une logique qui fonctionne sur un RenderTreeBuilder pour afficher leur sortie. Toutefois, un développeur peut implémenter manuellement une logique RenderTreeBuilder à l’aide de code C# procédural. Pour plus d’informations, consultez Scénarios avancés ASP.NET Core Blazor (construction d’arborescence de rendu).

Warning

L’utilisation de la logique de générateur d’arborescences de rendu manuel est considérée comme un scénario avancé et dangereux, non recommandé pour le développement de composants généraux.

Si du code RenderTreeBuilder est écrit, le développeur doit garantir son exactitude. Par exemple, le développeur doit s’assurer que :

  • Les appels à OpenElement et CloseElement sont correctement équilibrés.
  • Les attributs sont ajoutés uniquement aux emplacements appropriés.

Une logique de générateur d’arborescences de rendu manuelle incorrecte peut entraîner un comportement arbitraire non défini, notamment les blocages d’applications ou de serveurs et les vulnérabilités de sécurité.

Considérez la logique de générateur d’arborescences de rendu manuel au même niveau de complexité et avec le même niveau de danger que l’écriture manuelle de code d’assembly ou d’instructions Microsoft Intermediate Language (MSIL).

Ressources supplémentaires

†S’applique aux applications d’API web ASP.NET Core que les applications Blazor côté client utilisent pour la journalisation.