Control de errores en aplicaciones Blazor de ASP.NET Core

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

En este artículo se describe cómo Blazor administra las excepciones no controladas y cómo desarrollar aplicaciones que detecten y controlen los errores.

Errores detallados durante el desarrollo

Cuando una aplicación Blazor no funciona correctamente durante el desarrollo, recibir información detallada del error de la aplicación ayuda a solucionar el problema. Cuando se produce un error, en las aplicaciones Blazor se muestra una barra de color amarillo claro en la parte inferior de la pantalla:

  • Durante el desarrollo, la barra le dirige a la consola del explorador, donde puede ver la excepción.
  • En producción, la barra informa al usuario de que se ha producido un error y recomienda actualizar el explorador.

La interfaz de usuario para esta experiencia de control de errores forma parte de las plantillas de proyecto de Blazor. No todas las versiones de las plantillas de proyecto Blazor usan el atributo data-nosnippet para indicar a los exploradores que no almacenen en caché el contenido de la interfaz de usuario del error, pero todas las versiones de la documentación Blazor aplican el atributo.

En una Aplicación web Blazor, personalice la experiencia en el componente MainLayout. Debido a que el Asistente de etiquetas de entorno (por ejemplo, <environment include="Production">...</environment>) no es admitido en componentes Razor, el siguiente ejemplo inyecta IHostEnvironment para configurar mensajes de error para diferentes entornos.

En la parte superior de MainLayout.razor:

@inject IHostEnvironment HostEnvironment

Cree o modifique el marcado de error Blazor de la interfaz de usuario:

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

En una aplicación Blazor Server, personaliza la experiencia en el archivo Pages/_Host.cshtml. El siguiente ejemplo utiliza el Asistente de etiquetas de entorno para configurar mensajes de error para diferentes entornos.

En una aplicación Blazor Server, personalice la experiencia en el archivo Pages/_Layout.cshtml. El siguiente ejemplo utiliza el Asistente de etiquetas de entorno para configurar mensajes de error para diferentes entornos.

En una aplicación Blazor Server, personaliza la experiencia en el archivo Pages/_Host.cshtml. El siguiente ejemplo utiliza el Asistente de etiquetas de entorno para configurar mensajes de error para diferentes entornos.

Cree o modifique el marcado de error Blazor de la interfaz de usuario:

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

En una aplicación Blazor WebAssembly, personalice la experiencia en el archivo 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>

El elemento blazor-error-ui se oculta normalmente debido a la presencia del estilo display: none de la clase CSS blazor-error-ui en la hoja de estilos autogenerada de la aplicación. Cuando se produce un error, el marco aplica display: block al elemento.

El elemento blazor-error-ui se oculta normalmente debido a la presencia del estilo display: none de la clase CSS blazor-error-ui en la hoja de estilos del sitio en la carpeta wwwroot/css. Cuando se produce un error, el marco aplica display: block al elemento.

Errores de circuito detallados

Esta sección se aplica a las aplicaciones web Blazor que funcionan a través de un circuito.

Esta sección es aplicable a aplicaciones Blazor Server.

El error del lado cliente no incluye la pila de llamadas y no proporciona detalles sobre la causa del error, pero los registros del servidor sí contienen dicha información. La información de errores de circuito confidencial puede ponerse a disposición del cliente con fines de desarrollo mediante la habilitación de errores detallados.

Establezca CircuitOptions.DetailedErrors en true. Para obtener más información y un ejemplo, vea Guía de BlazorSignalR para ASP.NET Core.

Una alternativa a establecer CircuitOptions.DetailedErrors es definir la clave de configuración DetailedErrors en true en el archivo de configuración del entorno de Development de la aplicación (appsettings.Development.json). Del mismo modo, establezca el registro de lado servidor de SignalR (Microsoft.AspNetCore.SignalR) en Depurar o Seguimiento para el registro detallado de SignalR.

appsettings.Development.json:

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

La clave de configuración DetailedErrors también se puede establecer en true usando la variable de entorno ASPNETCORE_DETAILEDERRORS con un valor true en los servidores del entorno Development/Staging, o en el sistema local.

Advertencia

Evite en todo momento exponer información de errores a los clientes de Internet, ya que constituye un riesgo para la seguridad.

Errores detallados para la representación del lado servidor de componentes de Razor

Esta sección se aplica a Blazor Web Apps.

Use la opción RazorComponentsServiceOptions.DetailedErrors para controlar la generación de información detallada sobre errores para la representación del lado servidor de componentes de Razor. El valor predeterminado es false.

En el ejemplo siguiente se habilitan errores detallados:

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

Advertencia

Habilitar solo errores detallados en el entorno de Development. Los errores detallados pueden contener información confidencial sobre la aplicación que los usuarios malintencionados pueden usar en un ataque.

En el ejemplo anterior se proporciona un grado de seguridad estableciendo el valor de DetailedErrorsen función del valor devuelto por IsDevelopment. Cuando la aplicación está en el entorno Development, DetailedErrors se establece en true. Este enfoque no es infalible porque es posible hospedar una aplicación de producción en un servidor público en el entorno Development.

Administración de excepciones no controladas en el código de desarrollador

Para que una aplicación continúe después de un error, debe tener lógica de control de errores. En secciones posteriores de este artículo se describen posibles orígenes de excepciones no controladas.

En producción, en la interfaz de usuario no se representan mensajes de excepción del marco ni seguimientos de la pila. La representación de mensajes de excepción o seguimientos de la pila podría:

  • Divulgar información confidencial a los usuarios finales.
  • Ayudar a un usuario malintencionado a detectar debilidades en una aplicación que pueden poner en peligro la seguridad de la aplicación, el servidor o la red.

Excepciones de circuitos no controlados

Esta sección se aplica a las aplicaciones del lado servidor que funcionan a través de un circuito.

Los componentesRazor con interactividad de servidor activada son de estado en el servidor. Mientras los usuarios interactúan con el componente en el servidor, mantienen una conexión con el servidor conocida como circuito. El circuito contiene instancias de componentes activas, además de muchos otros aspectos del estado, como:

  • La salida representada más reciente de los componentes.
  • El conjunto actual de delegados de control de eventos que se pueden desencadenar por eventos del lado cliente.

Si un usuario abre la aplicación en varias pestañas del explorador, dicho usuario crea varios circuitos independientes.

Blazor trata la mayoría de las excepciones no controladas como graves para el circuito en el que se producen. Si se finaliza un circuito debido a una excepción no controlada, el usuario solo puede seguir interactuando con la aplicación si recarga la página para crear otro circuito. Los circuitos externos al que se ha finalizado, que son circuitos para otros usuarios u otras pestañas del explorador, no se ven afectados. Este escenario es parecido a cuando una aplicación de escritorio se bloquea. La aplicación bloqueada debe reiniciarse, pero otras no resultan afectadas.

El marco finaliza un circuito cuando se produce una excepción no controlada por los siguientes motivos:

  • Una excepción no controlada a menudo deja el circuito en un estado indefinido.
  • Después de una excepción no controlada no se puede garantizar el funcionamiento normal de la aplicación.
  • Es posible que aparezcan vulnerabilidades de seguridad en la aplicación si el circuito continúa en un estado indefinido.

Control de excepciones locales

Para el control global de excepciones, consulte las secciones siguientes:

Límites de error

Los límites de error proporcionan un enfoque práctico para controlar las excepciones. El componente ErrorBoundary:

  • Representa su contenido secundario cuando no se ha producido un error.
  • Representa la interfaz de usuario de error cuando se produce una excepción no controlada.

Para definir un límite de error, use el componente ErrorBoundary para encapsular el contenido existente. La aplicación sigue funcionando con normalidad, pero el límite de error controla las excepciones no controladas.

<ErrorBoundary>
    ...
</ErrorBoundary>

Para implementar un límite de error de forma global, agregue el límite alrededor del contenido del cuerpo del diseño principal de la aplicación.

En MainLayout.razor:

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

En la Web Apps Blazor con el límite de error solo se aplica a un componente estáticoMainLayout, el límite solo está activo durante la fase de representación estática del lado servidor (SSR estático). El límite no se activa solo porque un componente está más abajo en la jerarquía de componentes es interactivo. Para habilitar la interactividad ampliamente para el componente de MainLayout y el resto de los componentes más abajo de la jerarquía de componentes, habilite la representación interactiva para las instancias de componente HeadOutlet y Routes en el componente de App (Components/App.razor). En el ejemplo siguiente se adopta el modo de representación del servidor interactivo (InteractiveServer):

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Si prefiere no habilitar la interactividad del servidor en toda la aplicación desde el componente de Routes, coloque el límite de error más abajo en la jerarquía de componentes. Por ejemplo, coloque el límite de error alrededor del marcado en componentes individuales que habilitan la interactividad, no en el diseño principal de la aplicación. Los conceptos importantes que se tienen en cuenta son que dondequiera que se coloque el límite de error:

  • Si el límite de error no es interactivo, solo es capaz de activarse en el servidor durante la representación estática. Por ejemplo, el límite puede activarse cuando se produce un error en un método de ciclo de vida de componentes.
  • Si el límite de error es interactivo, es capaz de activarse para los componentes interactivos representados por el servidor que encapsula.

Considere el ejemplo siguiente, donde el componente Counter produce una excepción si el recuento se incrementa más allá de cinco.

En Counter.razor:

private void IncrementCount()
{
    currentCount++;

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

Si se produce la excepción no controlada para currentCount de más de cinco:

  • El error se registra normalmente (System.InvalidOperationException: Current count is too big!).
  • El límite de error controla la excepción.
  • La interfaz de usuario de error se muestra con el siguiente mensaje de error predeterminado: An error has occurred.

De forma predeterminada, el componente ErrorBoundary representa un elemento <div> vacío con la clase CSS de blazor-error-boundary para su contenido de error. Los colores, el texto y el icono de la interfaz de usuario predeterminada se definen mediante CSS en la hoja de estilos de la aplicación en la carpeta wwwroot, por lo que puede personalizar la interfaz de usuario de error.

Cambie el contenido de error por defecto estableciendo la propiedad ErrorContent:

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

Dado que el límite de error se define en el diseño de los ejemplos anteriores, la interfaz de usuario de error se ve independientemente de la página por la que navegue el usuario después de que se produzca el error. Se recomienda delimitar el ámbito de los límites de error en la mayoría de los escenarios. Si el ámbito de un límite de error es amplio, puede restablecerlo a un estado de no error en eventos de navegación de página posteriores llamando al método del límite de error Recover.

En MainLayout.razor:

...

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

...

@code {
    private ErrorBoundary? errorBoundary;

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

Para evitar el bucle infinito en el que la recuperación simplemente vuelve a renderizar un componente que lanza el error de nuevo, no llame Recover desde la lógica de renderizado. Llame solo a Recover cuando:

  • El usuario realiza un gesto de interfaz de usuario, como seleccionar un botón para indicar que desea reintentar un procedimiento o cuando el usuario navega a un nuevo componente.
  • La lógica adicional también borra la excepción. Cuando se vuelve a renderizar el componente, el error no vuelve a producirse.

Control de excepciones global alternativo

Una alternativa al uso de límites de error (ErrorBoundary) es pasar un componente de error personalizado como CascadingValue a los componentes secundarios. Una ventaja de usar un componente frente a usar un servicio insertado o una implementación de registrador personalizada es que un componente en cascada puede representar contenido y aplicar estilos CSS cuando se produce un error.

El siguiente componente Error de ejemplo simplemente registra errores, pero los métodos del componente pueden procesar los errores de la manera que requiera la aplicación, como mediante el uso de varios métodos de procesamiento de errores.

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

Nota:

Para obtener más información sobre RenderFragment, consulte Componentes de Razor de ASP.NET Core.

En el componente Routes, encapsule el componente Router (<Router>...</Router>) con el componente Error. Esto permite al componente Error descender en cascada a cualquier componente de la aplicación donde se reciba el componente Error como un CascadingParameter.

En Routes.razor:

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

En el componente App, encapsule el componente Router (<Router>...</Router>) con el componente Error. Esto permite al componente Error descender en cascada a cualquier componente de la aplicación donde se reciba el componente Error como un CascadingParameter.

En App.razor:

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

Para procesar los errores de un componente:

  • Designe el componente Error como un CascadingParameter en el bloque @code. En un componente Counter de ejemplo de una aplicación basada en una plantilla de proyecto Blazor, agregue la siguiente propiedad de Error:

    [CascadingParameter]
    public Error? Error { get; set; }
    
  • Llame a un método de procesamiento de errores en cualquier bloque catch con un tipo de excepción adecuado. El componente Error de ejemplo ofrece solamente un único método ProcessError, pero el componente de procesamiento de errores puede proporcionar todos los métodos de procesamiento de errores que sean necesarios para abordar los requisitos de procesamiento de los errores alternativos en la aplicación. En el siguiente componente Counter de ejemplo, se produce una excepción y se detecta cuando el recuento es mayor que cinco:

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

Si usamos el componente Error anterior con los cambios realizados previamente en un componente Counter, la consola de las herramientas de desarrollo del explorador indica el error capturado y registrado:

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

Si el método ProcessError participa directamente en la renderización, como mostrar una barra de mensaje de error personalizada o cambiar los estilos CSS de los elementos representados, llame StateHasChanged al final del método ProcessErrors para volver a renderizar la IU.

Dado que los enfoques de esta sección administran los errores con una declaración try-catch, la conexión de una aplicación SignalR entre el cliente y el servidor no se interrumpe cuando se produce un error y el circuito sigue vivo. Otras excepciones no controladas siguen resultando irrecuperables para un circuito. Para más información, consulte la sección sobre cómo reacciona un circuito ante excepciones no tratadas.

Una aplicación puede utilizar un componente de procesamiento de errores como valor en cascada para procesar los errores de forma centralizada.

El siguiente componente Error se pasa a sí mismo como un CascadingValue a los componentes secundarios. En el siguiente ejemplo simplemente se registra el error, pero los métodos del componente pueden procesar los errores de la manera que requiera la aplicación, incluido el uso de varios métodos de procesamiento de errores. Una ventaja de usar un componente frente a usar un servicio insertado o una implementación de registrador personalizada es que un componente en cascada puede representar contenido y aplicar estilos CSS cuando se produce un error.

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

Nota:

Para obtener más información sobre RenderFragment, consulte Componentes de Razor de ASP.NET Core.

En el componente App, ajuste el componente Router con el componente Error. Esto permite al componente Error desplazarse en cascada a cualquier componente inferior de la aplicación en el que el componente Error se reciba como un parámetro CascadingParameter.

App.razor:

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

Para procesar los errores de un componente:

  • Designe el componente Error como un parámetro CascadingParameter en el bloque @code:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Llame a un método de procesamiento de errores en cualquier bloque catch con un tipo de excepción adecuado. El componente Error de ejemplo ofrece solamente un único método ProcessError, pero el componente de procesamiento de errores puede proporcionar todos los métodos de procesamiento de errores que sean necesarios para abordar los requisitos de procesamiento de los errores alternativos en la aplicación.

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

Si usamos el componente Error y el método ProcessError del ejemplo anterior, la consola de las herramientas de desarrollo del explorador indica el error capturado y registrado:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Referencia a objeto no establecida como instancia de un objeto.

Si el método ProcessError participa directamente en la representación (por ejemplo, mostrando una barra de mensajes de error personalizada o cambiando los estilos CSS de los elementos representados), llame a StateHasChanged al final del método ProcessErrors para representar la interfaz de usuario.

Como los enfoques de esta sección controlan los errores con una instrucción try-catch, la conexión SignalR de una aplicación Blazor entre el cliente y el servidor no se interrumpe cuando se produce un error, y el circuito permanece activo. Una excepción no controlada es grave para un circuito. Para más información, consulte la sección sobre cómo reacciona un circuito ante excepciones no tratadas.

Registro de errores con un proveedor persistente

Si se produce una excepción no controlada, la excepción se registra en las instancias de ILogger configuradas en el contenedor de servicios. De forma predeterminada, las aplicaciones Blazor registran en la salida de la consola con el proveedor de registro de la consola. Considere la posibilidad de registrar en una ubicación del servidor (o API web backend para aplicaciones del lado del cliente) con un proveedor que administre el tamaño y la rotación de los registros. Opcionalmente, la aplicación puede usar un servicio de administración del rendimiento de la aplicación, como Azure Application Insights (Azure Monitor).

Nota

Las funciones nativas de Application Insights para admitir aplicaciones del lado del cliente y la Blazorcompatibilidad con marcos nativos para Google Analytics podrían estar disponibles en futuras versiones de estas tecnologías. Para obtener más información, vea Compatibilidad de Application Insights en el lado cliente de Blazor WebAssembly (Microsoft/ApplicationInsights-dotnet #2143) y Diagnósticos y análisis web (dotnet/aspnetcore #5461), que incluye vínculos a implementaciones de la comunidad. Mientras tanto, una aplicación del lado del cliente puede utilizar el SDK JavaScript de Application Insights con JS interoperabilidad para registrar errores directamente en Application Insights desde una aplicación del lado del cliente.

Durante el desarrollo en una aplicación Blazor que opera sobre un circuito, la aplicación suele enviar todos los detalles de las excepciones a la consola del navegador para ayudar en la depuración. En producción, los errores detallados no se envían a los clientes, pero todos los detalles de una excepción se registran en el servidor.

Debe decidir qué incidentes registrar y el nivel de gravedad de los incidentes registrados. Es posible que los usuarios hostiles puedan desencadenar errores deliberadamente. Por ejemplo, no registre un incidente de un error en el que se proporcione un valor ProductId desconocido en la dirección URL de un componente que muestra los detalles del producto. No todos los errores se deben tratar como incidentes para el registro.

Para más información, vea los siguientes artículos:

‡Se aplica a las aplicaciones del lado del servidorBlazor y a otras aplicaciones ASP.NET Core del lado del servidor que son aplicaciones de backend de API web para Blazor. Las aplicaciones del lado del cliente pueden atrapar y enviar información de error en el cliente a una API web, que registra la información de error en un proveedor de registro persistente.

Si se produce una excepción no controlada, la excepción se registra en las instancias de ILogger configuradas en el contenedor de servicios. De forma predeterminada, las aplicaciones Blazor registran en la salida de la consola con el proveedor de registro de la consola. Valore la posibilidad de realizar los registros en una ubicación más permanente en el servidor. Para ello, envíe la información de los errores a una API web de back-end que use un proveedor de registros con rotación de estos y administración de su tamaño. Opcionalmente, la aplicación de API web de back-end puede usar un servicio de administración del rendimiento de la aplicación, como Azure Application Insights (Azure Monitor)†, para registrar la información de los errores que recibe de los clientes.

Debe decidir qué incidentes registrar y el nivel de gravedad de los incidentes registrados. Es posible que los usuarios hostiles puedan desencadenar errores deliberadamente. Por ejemplo, no registre un incidente de un error en el que se proporcione un valor ProductId desconocido en la dirección URL de un componente que muestra los detalles del producto. No todos los errores se deben tratar como incidentes para el registro.

Para más información, vea los siguientes artículos:

†Las funciones de información sobre Application Insights nativas para admitir aplicaciones del lado del cliente y la Blazorcompatibilidad con marcos nativos para Google Analytics podrían estar disponibles en futuras versiones de estas tecnologías. Para obtener más información, vea Compatibilidad de Application Insights en el lado cliente de Blazor WebAssembly (Microsoft/ApplicationInsights-dotnet #2143) y Diagnósticos y análisis web (dotnet/aspnetcore #5461), que incluye vínculos a implementaciones de la comunidad. Mientras tanto, una aplicación del lado del cliente puede utilizar el SDK JavaScript de Application Insights con JS interoperabilidad para registrar errores directamente en Application Insights desde una aplicación del lado del cliente.

†Corresponde a las aplicaciones ASP.NET Core del lado servidor que sean de back-end de API web de aplicaciones Blazor. Las aplicaciones del lado cliente capturan y envían información de errores a una API web, que registra la información de los errores en un proveedor de registro persistente.

Lugares donde se pueden producir errores

El código de la aplicación y el marco pueden desencadenar excepciones no controladas en cualquiera de las siguientes ubicaciones, que se describen más adelante en las secciones de este artículo:

Creación de instancias de componentes

Cuando Blazor crea una instancia de un componente:

  • Se invoca el constructor del componente.
  • Se invocan los constructores de servicios de inserción de dependencias proporcionados al constructor del componente a través de la directiva @inject o el atributo [Inject].

Un error en un establecedor o constructor ejecutado en relación con una propiedad [Inject] produce una excepción no controlada e impide al marco crear una instancia del componente. Si la aplicación funciona sobre un circuito, el circuito falla. Si la lógica del constructor puede lanzar excepciones, la aplicación debe atrapar las excepciones utilizando una declaración try-catch con administración de errores y registro.

Métodos de ciclo de vida

Durante la vigencia de un componente, Blazor invoca métodos de ciclo de vida. Si cualquier método de ciclo de vida inicia una excepción, de forma sincrónica o asincrónica, la excepción es grave para un circuito de . Para que los componentes traten los errores de los métodos de ciclo de vida, agregue lógica de control de errores.

En el ejemplo siguiente, donde OnParametersSetAsync llama a un método para obtener un producto:

  • Una instrucción try-catch controla una excepción que se inicia en el método ProductRepository.GetProductByIdAsync.
  • Cuando se ejecuta el bloque catch:
    • loadFailed se establece en true, que se usa para mostrar un mensaje de error al usuario.
    • El error no se registra.
@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);
    }
}

Lógica de representación

El marcado declarativo de un archivo de componente Razor (.razor) se compila en un método de C# denominado BuildRenderTree. Cuando se representa un componente, BuildRenderTree ejecuta y genera una estructura de datos que describe los elementos, el texto y los componentes secundarios del componente representado.

La lógica de representación puede iniciar una excepción. Un ejemplo de este escenario se produce cuando se evalúa @someObject.PropertyName pero @someObject es null. Para las aplicaciones Blazor que operan sobre un circuito, una excepción no controlada lanzada por la lógica de renderizado es fatal para el circuito de la aplicación.

Para evitar una excepción NullReferenceException en la lógica de representación, busque un objeto null antes de acceder a sus miembros. En el ejemplo siguiente, no se accede a las propiedades person.Address si person.Address es 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>
}

En el código anterior se supone que person no es null. A menudo, la estructura del código garantiza que un objeto existe en el momento en que se representa el componente. En esos casos, no es necesario comprobar null en la lógica de representación. En el ejemplo anterior, es posible que se garantice la existencia de person porque se crea person al generar una instancia del componente, como refleja el siguiente ejemplo:

@code {
    private Person person = new();

    ...
}

Controladores de eventos

El código del lado cliente desencadena invocaciones de código de C# cuando se crean controladores de eventos mediante:

  • @onclick
  • @onchange
  • Otros atributos @on...
  • @bind

Es posible que el código del controlador de eventos inicie una excepción no controlada en estos escenarios.

Si la aplicación llama a código que podría generar un error por motivos externos, capture las excepciones mediante una instrucción try-catch con control de errores y registro.

Si un controlador de eventos produce una excepción no controlada (por ejemplo, se produce un error en una consulta de base de datos) que el código de desarrollador no detecta y controla:

  • El marco registra la excepción.
  • En una aplicación Blazor que opera sobre un circuito, la excepción es fatal para el circuito de la aplicación.

Eliminación de componentes

Un componente se puede quitar de la interfaz de usuario, por ejemplo, porque el usuario ha navegado a otra página. Cuando un componente que implementa System.IDisposable se quita de la interfaz de usuario, el marco de trabajo llama al método Dispose del componente.

Si el método del componente Dispose lanza una excepción no controlada en una aplicación Blazor que opera sobre un circuito, la excepción es fatal para el circuito de la aplicación.

Si la lógica de eliminación puede lanzar excepciones, la aplicación debe atrapar las excepciones utilizando una declaración try-catch con administración de errores y registro.

Para más información sobre la eliminación de componentes, vea Ciclo de vida de componentes Razor de ASP.NET Core.

Interoperabilidad de JavaScript

El marco Blazor registra IJSRuntime. IJSRuntime.InvokeAsync permite que el código de .NET realice llamadas asincrónicas al entorno de ejecución de JavaScript (JS) en el explorador del usuario.

Se aplican las condiciones siguientes al control de errores con InvokeAsync:

  • Si una llamada a InvokeAsync produce un error de forma sincrónica, se produce una excepción de .NET. Se puede producir un error en una llamada a InvokeAsync, por ejemplo, porque no se puedan serializar los argumentos proporcionados. El código del desarrollador debe detectar la excepción. Si el código de una aplicación en un manejador de eventos o en un método del ciclo de vida de un componente no maneja una excepción en una aplicación Blazor que opera sobre un circuito, la excepción resultante es fatal para el circuito de la aplicación.
  • Si se produce un error en una llamada a InvokeAsync de forma asincrónica, se produce un error en el objeto Task de .NET. Se puede producir un error en una llamada a InvokeAsync, por ejemplo, porque el código de JS inicia una excepción o devuelve un objeto Promise que se ha completado como rejected. El código del desarrollador debe detectar la excepción. Si utiliza el operador await, considere envolver la llamada al método en una declaración try-catch con administración de errores y registro. De lo contrario, en una aplicación Blazor que funciona sobre un circuito, el código que falla provoca una excepción no controlada que es fatal para el circuito de la aplicación.
  • Por defecto, las llamadas a InvokeAsync deben completarse en un plazo determinado o, de lo contrario, la llamada se agota. El tiempo de espera por defecto es de un minuto. El tiempo de expiración protege al código de una pérdida en la conectividad de red o de código JS que nunca devuelve un mensaje de finalización. Si se agota el tiempo de espera de la llamada, se produce un error en el objeto System.Threading.Tasks resultante con una excepción OperationCanceledException. Capture y procese la excepción con el registro.

Del mismo modo, el código de JS puede iniciar llamadas a métodos de .NET indicados por el atributo [JSInvokable]. Si estos métodos de .NET inician una excepción no controlada:

  • En una aplicación Blazor que opera sobre un circuito, la excepción no se trata como fatal para el circuito de la aplicación.
  • Promise de JS se ha rechazado.

Tiene la opción de usar el código de control de errores en el lado de .NET o en el de JS de la llamada de método.

Para obtener más información, vea los artículos siguientes:

Representación previa

Los componentes Razor se representan previamente por defecto, de modo que su marcado HTML representado se devuelve como parte de la petición HTTP inicial del usuario.

En una aplicación Blazor que funciona sobre un circuito, la representación previa funciona de la siguiente manera:

  • Se crea un circuito para todos los componentes con representación previa que forman parte de la misma página.
  • Se genera el código HTML inicial.
  • El circuito se trata como disconnected hasta que el explorador del usuario establece una conexión de SignalR al mismo servidor. Cuando se establece la conexión, se reanuda la interactividad en el circuito y se actualiza el marcado HTML de los componentes.

En el caso de los componentes representados previamente del lado del cliente, la representación previa funciona de la siguiente manera:

  • Generación de HTML inicial en el servidor para todos los componentes representados previamente que forman parte de la misma página.
  • Hacer que el componente sea interactivo en el cliente después de que el explorador haya cargado el código compilado de la aplicación y el entorno de ejecución de .NET (si aún no está cargado) en segundo plano.

Si un componente inicia una excepción no controlada durante la representación previa, por ejemplo, durante un método de ciclo de vida o en la lógica de representación:

  • En una aplicación Blazor que funciona sobre un circuito, la excepción es fatal para el circuito. En el caso de los componentes representados previamente del lado del cliente, la excepción impide la representación del componente.
  • La excepción se inicia en la pila de llamadas desde ComponentTagHelper.

En circunstancias normales, cuando se produce un error en la representación previa, continuar con la generación y representación del componente no tiene sentido, ya que un componente en funcionamiento no se puede representar.

Para tolerar los errores que se puedan producir durante la representación previa, se debe colocar la lógica de control de errores dentro de un componente que pueda iniciar excepciones. Use instrucciones try-catch con control de errores y registro. En lugar de encapsular ComponentTagHelper en una instrucción try-catch, coloque la lógica de control de errores en el componente representado por ComponentTagHelper.

Escenarios avanzados

Representación recursiva

Los componentes se pueden anidar de forma recursiva. Esto resulta útil para representar estructuras de datos recursivas. Por ejemplo, un componente TreeNode puede representar más componentes TreeNode para cada uno de los elementos secundarios del nodo.

Al realizar la representación de forma recursiva, evite patrones de codificación que produzcan una recursión infinita:

  • No represente de forma recursiva una estructura de datos que contenga un ciclo. Por ejemplo, no represente un nodo de árbol cuyos elementos secundarios se incluyan a sí mismos.
  • No cree una cadena de diseños que contengan un ciclo. Por ejemplo, no debe crear un diseño cuyo diseño sea él mismo.
  • No permita que un usuario final incumpla las invariables de recursión (reglas) a través de entradas de datos malintencionadas o llamadas de interoperabilidad de JavaScript.

Bucles infinitos durante la representación:

  • Hace que el proceso de representación continúe de manera indefinida.
  • Equivale a crear un bucle sin terminar.

En estos escenarios, el Blazor falla y por lo general los intentos de:

  • Consumir el máximo tiempo de CPU que permita el sistema operativo, de manera indefinida.
  • Consumir una cantidad ilimitada de memoria. El consumo de memoria ilimitada es equivalente al escenario en el que un bucle sin terminar agrega entradas a una colección en cada iteración.

Para evitar patrones de recursión, asegúrese de que el código de representación recursiva contenga condiciones de detención adecuadas.

Lógica de árbol de representación personalizada

La mayoría de los componentes Razor se implementan como archivos de componente Razor (.razor), y el marco los compila para generar una lógica que opere en un objeto RenderTreeBuilder para representar su salida. Con todo, un desarrollador puede implementar de forma manual una lógica de RenderTreeBuilder mediante código de C# por procedimientos. Para obtener más información, vea Escenarios avanzados de ASP.NET Core Blazor (construcción de árboles de representación).

Advertencia

El uso de la lógica del generador de árboles de representación manual se considera un escenario avanzado y no seguro, no recomendado para el desarrollo de componentes generales.

Si se escribe código de RenderTreeBuilder, el desarrollador debe garantizar la corrección del código. Por ejemplo, el desarrollador debe asegurarse de que:

  • Las llamadas a OpenElement y CloseElement se equilibran correctamente.
  • Los atributos solo se agregan en los lugares correctos.

La lógica incorrecta del constructor manual del árbol de la representación puede causar un comportamiento arbitrario indefinido, incluyendo bloqueos, cuelgues de la aplicación o del servidor y vulnerabilidades de seguridad.

Valore la posibilidad de usar una lógica de generador de árbol de representación manual en el mismo nivel de complejidad y con el mismo nivel de peligro, como escribir a mano código de ensamblado o instrucciones en lenguaje intermedio de Microsoft (MSIL).

Recursos adicionales

†Se aplica a las aplicaciones de API web ASP.NET Core de back-end que usan aplicaciones Blazor del lado cliente para el registro.