Compartir a través de


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, consulta la versión .NET 8 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta 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, consulta 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 Blazor Web App, personaliza 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

Crea o modifica 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, personaliza 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.

Crea o modifica 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, personaliza 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 Blazor Web App 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.

Establece CircuitOptions.DetailedErrors en true. Para obtener más información y un ejemplo, consulta 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, establece 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 es aplicable a Blazor Web App.

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 conocer los enfoques para controlar las excepciones globalmente, 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 cualquier componente produce una excepción no controlada dentro del límite de error.

Para definir un límite de error, use el componente ErrorBoundary para encapsular uno o varios otros componentes. El límite de error administra las excepciones no controladas producidas por los componentes que contiene.

<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 Blazor Web App con el límite de error solo aplicado a un componente MainLayout estático, el límite solo está activo durante la 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.

No se puede aplicar un modo de representación interactiva al componente de MainLayout porque el parámetro Body del componente es un delegado RenderFragment, que es código arbitrario y no se puede serializar. Para habilitar la interactividad ampliamente para el componente MainLayout y el objeto rest de los componentes más abajo de la jerarquía de componentes, la aplicación debe adoptar un modo de representación interactiva global aplicando el modo de representación interactiva a las instancias de componente HeadOutlet y Routes en el componente raíz de la aplicación, que normalmente es el componente App. En el ejemplo siguiente se adopta el modo de representación del servidor interactivo (InteractiveServer) globalmente.

En Components/App.razor:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Si prefieres no habilitar la interactividad global, coloca el límite de error más allá de la jerarquía de componentes. Los conceptos importantes que se tienen en cuenta son que dondequiera que se coloque el límite de error:

  • Si el componente en el que se coloca el límite de error no es interactivo, el límite de error solo es capaz de activarse en el servidor durante el SSR estático. Por ejemplo, el límite puede activarse cuando se produce un error en un método de ciclo de vida de componentes, pero no para un evento desencadenado por la interactividad del usuario dentro del componente, como un error producido por un controlador de clic de botón.
  • Si el componente en el que se coloca el límite de error es interactivo, el límite de error es capaz de activarse para componentes interactivos que encapsula.

Nota:

Las consideraciones anteriores no son pertinentes para las aplicaciones de Blazor WebAssembly independientes porque la representación del lado cliente (CSR) de una aplicación de Blazor WebAssembly es completamente interactiva.

Considera el ejemplo siguiente, donde un componente de contador incrustado detecta una excepción producida por un límite de error en el componente Home, que adopta un modo de representación interactivo.

EmbeddedCounter.razor:

<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

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

Home.razor:

@page "/"
@rendermode InteractiveServer

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>

Considera el ejemplo siguiente, donde un componente de contador incrustado detecta una excepción producida por un límite de error en el componente Home.

EmbeddedCounter.razor:

<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

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

Home.razor:

@page "/"

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>

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 predeterminada se representa mediante el límite de error.

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

La interfaz de usuario de error predeterminada representada por un límite de error, que tiene un fondo rojo, el texto

Para cambiar el contenido de error predeterminado:

  • Encapsula los componentes del límite de error en la propiedad ChildContent.
  • Establece propiedad ErrorContent en el contenido del error.

En el ejemplo siguiente se incluye el componente EmbeddedCounter y se proporciona contenido de error personalizado:

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

En el ejemplo anterior, la hoja de estilos de la aplicación incluye presumiblemente una clase CSS de errorUI para aplicar estilo al contenido. El contenido del error se representa desde la propiedad ErrorContent sin un elemento de nivel de bloque. Un elemento de nivel de bloque, como un elemento division (<div>) o un elemento paragraph (<p>), puede encapsular el marcado de contenido de error, pero no es necesario.

Opcionalmente, usa el contexto (@context) de ErrorContent para obtener datos de error:

<ErrorContent>
    @context.HelpLink
</ErrorContent>

ErrorContent también puede asignar un nombre al contexto. En el ejemplo siguiente, el contexto se denomina exception:

<ErrorContent Context="exception">
    @exception.HelpLink
</ErrorContent>

Advertencia

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

Si el límite de error se define en el diseño de la aplicación, la interfaz de usuario de error se ve independientemente de la página a la que navega 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, puedes 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. Llama 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 que se ejecuta también borra la excepción. Cuando se vuelve a renderizar el componente, el error no vuelve a producirse.

En el ejemplo siguiente se permite al usuario recuperarse de la excepción con un botón:

<ErrorBoundary @ref="errorBoundary">
    <ChildContent>
        <EmbeddedCounter />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-danger" role="alert">
            <p class="fs-3 fw-bold">😈 A rotten gremlin got us. Sorry!</p>
            <p>@context.HelpLink</p>
            <button class="btn btn-info" @onclick="_ => errorBoundary?.Recover()">
                Clear
            </button>
        </div>
    </ErrorContent>
</ErrorBoundary>

@code {
    private ErrorBoundary? errorBoundary;
}

También puedes subclasificar ErrorBoundary para el procesamiento personalizado invalidando OnErrorAsync. El ejemplo siguiente simplemente registra el error, pero puedes implementar cualquier código de control de errores que desees. Puedes quitar la línea que devuelve CompletedTask si el código espera una tarea asincrónica.

CustomErrorBoundary.razor:

@inherits ErrorBoundary
@inject ILogger<CustomErrorBoundary> Logger

@if (CurrentException is null)
{
    @ChildContent
}
else if (ErrorContent is not null)
{
    @ErrorContent(CurrentException)
}

@code {
    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
        return Task.CompletedTask;
    }
}

El ejemplo anterior también se puede implementar como una clase.

CustomErrorBoundary.cs:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace BlazorSample;

public class CustomErrorBoundary : ErrorBoundary
{
    [Inject]
    ILogger<CustomErrorBoundary> Logger {  get; set; } = default!;

    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
        return Task.CompletedTask;
    }
}

Cualquiera de las implementaciones anteriores usadas en un componente:

<CustomErrorBoundary>
    ...
</CustomErrorBoundary>

Control de excepciones global alternativo

El enfoque descrito en esta sección se aplica a Blazor Server, Blazor WebAssemblyy Blazor Web App que adoptan un modo de representación interactiva global (InteractiveServer, InteractiveWebAssembly o InteractiveAuto). El enfoque no funciona con Blazor Web App que adoptan modos de representación por página o componente o representación estática del lado servidor (SSR estático) porque el enfoque se basa en un CascadingValue/CascadingParameter, que no funciona en los límites del modo de representación o con componentes que adoptan SSR estático.

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

ProcessError.razor:

@inject ILogger<ProcessError> Logger

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

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

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

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

Nota:

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

Al usar este enfoque en un objeto Blazor Web App, abre el componente Routes y encapsula el componente Router (<Router>...</Router>) con el componente ProcessError. Esto permite al componente ProcessError descender en cascada a cualquier componente de la aplicación donde se reciba el componente ProcessError como un CascadingParameter.

En Routes.razor:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

Al usar este enfoque en una aplicación Blazor Server o Blazor WebAssembly, abre el componente App, encapsula el componente Router (<Router>...</Router>) con el componente ProcessError. Esto permite al componente ProcessError descender en cascada a cualquier componente de la aplicación donde se reciba el componente ProcessError como un CascadingParameter.

En App.razor:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

Para procesar los errores de un componente:

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

    [CascadingParameter]
    public ProcessError? ProcessError { get; set; }
    
  • Llama a un método de procesamiento de errores en cualquier bloque catch con un tipo de excepción adecuado. El componente ProcessError de ejemplo ofrece solamente un único método LogError, 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 ejemplo de bloque Counter de componente @code se incluye el parámetro ProcessError en cascada y se intercepta una excepción para el registro cuando el recuento es mayor que cinco:

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

El error registrado:

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

Si el método LogError participa directamente en la renderización, como mostrar una barra de mensaje de error personalizada o cambiar los estilos CSS de los elementos representados, llama a StateHasChanged al final del método LogError 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 obtener más información, consulta 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 ProcessError 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.

ProcessError.razor:

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

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

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

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

Nota:

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

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

App.razor:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

Para procesar los errores de un componente:

  • Designa el componente ProcessError como un parámetro CascadingParameter en el bloque @code:

    [CascadingParameter]
    public ProcessError ProcessError { get; set; }
    
  • Llama a un método de procesamiento de errores en cualquier bloque catch con un tipo de excepción adecuado. El componente ProcessError de ejemplo ofrece solamente un único método LogError, 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)
    {
        ProcessError.LogError(ex);
    }
    

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

fail: {COMPONENT NAMESPACE}.Shared.ProcessError[0]
ProcessError.LogError: System.NullReferenceException Message: Object reference not set to an instance of an object.

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

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 obtener más información, consulta 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. Las aplicaciones Blazor registran en la salida de la consola con el proveedor de registro de la consola. Considera 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, consulta Compatibilidad de Application Insights en el lado cliente de Blazor WASM (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.

Debes 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 registres 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 obtener más información, consulta 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. Las aplicaciones Blazor registran en la salida de la consola con el proveedor de registro de la consola. Valora la posibilidad de realizar los registros en una ubicación más permanente en el servidor. Para ello, envía 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.

Debes 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 registres 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 obtener más información, consulta 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, consulta Compatibilidad de Application Insights en el lado cliente de Blazor WASM (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?}"
@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 controlador de eventos o en un método del ciclo de vida de un componente no controla 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 utilizas el operador await, considera ajustar 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.
  • 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. Captura y procesa 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.

Tienes 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, consulta 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. Usa instrucciones try-catch con control de errores y registro. En lugar de encapsular ComponentTagHelper en una instrucción try-catch, coloca 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, evita patrones de codificación que produzcan una recursión infinita:

  • No representes de forma recursiva una estructura de datos que contenga un ciclo. Por ejemplo, no representes un nodo de árbol cuyos elementos secundarios se incluyan a sí mismos.
  • No crees 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, que la aplicación o el servidor dejen de responder 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.