Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Nota:
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 10 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la directiva de compatibilidad de .NET y .NET Core. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Blazor usa un contexto de sincronización (SynchronizationContext) para aplicar un único subproceso lógico de ejecución. Los métodos de ciclo de vida de un componente y las devoluciones de llamada de eventos que Blazor genere se ejecutan en el contexto de sincronización.
El contexto de sincronización del servidor de Blazor intenta emular un entorno de un solo subproceso para que coincida con el modelo WebAssembly en el explorador, que es de un solo subproceso. Esta emulación se limita solo a un circuito individual, lo que significa que dos circuitos diferentes se pueden ejecutar en paralelo. En cualquier momento dado dentro de un circuito, el trabajo se realiza en exactamente un subproceso, lo que da la impresión de un único subproceso lógico. No se ejecutan dos operaciones simultáneamente dentro del mismo circuito.
Un único subproceso lógico de ejecución no implica un único flujo de control asincrónico. Un componente es reentrante en cualquier punto donde espera un Task incompleto. Se puede llamar a los métodos de ciclo de vida o métodos de eliminación de componentes antes de que se reanude el flujo de control asincrónico después de haber esperado Task a que se complete. Por lo tanto, un componente debe asegurarse de que está en un estado válido antes de esperar a que un elemento esté potencialmente incompleto Task. En concreto, un componente debe asegurarse de que está en un estado válido para la representación cuando OnInitializedAsync o OnParametersSetAsync devuelve. Si alguno de estos métodos devuelve un elemento incompleto Task, debe asegurarse de que la parte del método que se completa sincrónicamente deja el componente en un estado válido para la representación.
Otra implicación de los componentes reentrantes es que un método no puede diferir un Task hasta después de que el método haya devuelto al pasarlo a ComponentBase.InvokeAsync. La llamada a ComponentBase.InvokeAsync solo puede aplazar el Task hasta que se alcance el siguiente operador await.
Los componentes pueden implementar IDisposable o IAsyncDisposable para llamar a métodos asincrónicos mediante un CancellationToken de un CancellationTokenSource que se cancela cuando se descarta el componente. Sin embargo, esto realmente depende del escenario. Es necesario que el autor del componente determine si ese es el comportamiento correcto. Por ejemplo, si implementa un SaveButton componente que conserva algunos datos locales en una base de datos cuando se selecciona un botón guardar, el autor del componente puede intentar descartar los cambios si el usuario selecciona el botón y navega rápidamente a otra página, lo que podría eliminar el componente antes de que se complete el guardado asincrónico.
Un componente descartable puede comprobar la eliminación después de esperar a que Task no reciba el componente CancellationToken. Los componentes incompletos Tasktambién pueden impedir la recolección de basura de un componente desechado.
ComponentBase omite las excepciones causadas por Task la cancelación (más precisamente, omite todas las excepciones si se cancelan los Task esperados), por lo que los métodos del componente no necesitan controlar TaskCanceledException y OperationCanceledException.
ComponentBase no puede seguir las instrucciones anteriores porque no conceptualiza lo que constituye un estado válido para un componente derivado y no implementa IDisposable ni IAsyncDisposable. Si OnInitializedAsync devuelve un valor incompleto Task que no usa CancellationToken y el componente se elimina antes de que Task se complete, ComponentBase todavía llama OnParametersSet y espera OnParametersSetAsync. Si un componente descartable no usa un CancellationToken, OnParametersSet y OnParametersSetAsync deben comprobar si el componente se ha descartado.
Evitar las llamadas de bloqueo de subprocesos
Por lo general, no llames a los métodos siguientes en componentes. Los métodos siguientes bloquean el subproceso de ejecución y, por tanto, impiden que la aplicación reanude el trabajo hasta que se complete Task:
Nota:
Los ejemplos de documentación de Blazor que usan los métodos de bloqueo de subprocesos mencionados en esta sección solo usan los métodos con fines de demostración, no como guía de codificación recomendada. Por ejemplo, algunas demostraciones de código de componentes simulan un proceso de ejecución larga mediante una llamada a Thread.Sleep.
Invocación de métodos de componentes externamente para actualizar el estado
En caso de que un componente deba actualizarse en función de un evento externo, como un temporizador u otras notificaciones, usa el método InvokeAsync, que envía la ejecución de código al contexto de sincronización de Blazor. Consideremos, por ejemplo, el siguiente servicio de notificador capaz de notificar el estado actualizado a cualquier componente de escucha. Se puede llamar al método Update desde cualquier lugar de la aplicación.
TimerService.cs:
namespace BlazorSample;
public class TimerService(NotifierService notifier,
ILogger<TimerService> logger) : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger = logger;
private readonly NotifierService notifier = notifier;
private PeriodicTimer? timer;
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("ElapsedCount {Count}", elapsedCount);
}
}
}
}
public void Stop()
{
if (timer is not null)
{
timer.Dispose();
timer = null;
logger.LogInformation("Stopped");
}
}
public void Dispose()
{
timer?.Dispose();
// The following prevents derived types that introduce a
// finalizer from needing to re-implement IDisposable.
GC.SuppressFinalize(this);
}
}
namespace BlazorSample;
public class TimerService(NotifierService notifier,
ILogger<TimerService> logger) : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger = logger;
private readonly NotifierService notifier = notifier;
private PeriodicTimer? timer;
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("ElapsedCount {Count}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
// The following prevents derived types that introduce a
// finalizer from needing to re-implement IDisposable.
GC.SuppressFinalize(this);
}
}
public class TimerService : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
}
}
public class TimerService : IDisposable
{
private int elapsedCount;
private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public async Task Start()
{
if (timer is null)
{
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
{
while (await timer.WaitForNextTickAsync())
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
}
}
}
public void Dispose()
{
timer?.Dispose();
}
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public void Start()
{
if (timer is null)
{
timer = new();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
public void Dispose()
{
timer?.Dispose();
}
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
{
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
{
this.notifier = notifier;
this.logger = logger;
}
public void Start()
{
if (timer is null)
{
timer = new Timer();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
}
public void Dispose()
{
timer?.Dispose();
}
}
NotifierService.cs:
namespace BlazorSample;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
namespace BlazorSample;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;
public class NotifierService
{
public async Task Update(string key, int value)
{
if (Notify != null)
{
await Notify.Invoke(key, value);
}
}
public event Func<string, int, Task> Notify;
}
Registra los servicios:
Para el desarrollo del lado cliente, registra los servicios como singletons en el archivo del lado cliente
Program:builder.Services.AddSingleton<NotifierService>(); builder.Services.AddSingleton<TimerService>();Para el desarrollo del lado servidor, registra los servicios como con ámbito en el archivo
Programdel lado servidor:builder.Services.AddScoped<NotifierService>(); builder.Services.AddScoped<TimerService>();
Usa el elemento NotifierService para actualizar un componente.
Notifications.razor:
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<PageTitle>Notifications</PageTitle>
<h1>Notifications Example</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<button @onclick="StopTimer">Stop Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized() => Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer() => _ = Task.Run(Timer.Start);
private void StopTimer() => Timer.Stop();
public void Dispose() => Notifier.Notify -= OnNotify;
}
Notifications.razor:
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<PageTitle>Notifications</PageTitle>
<h1>Notifications Example</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized() => Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer() => _ = Task.Run(Timer.Start);
public void Dispose() => Notifier.Notify -= OnNotify;
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
_ = Task.Run(Timer.Start);
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
_ = Task.Run(Timer.Start);
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key is not null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
Timer.Start();
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
ReceiveNotifications.razor:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
<p>
Status:
@if (lastNotification.key != null)
{
<span>@lastNotification.key = @lastNotification.value</span>
}
else
{
<span>Awaiting notification</span>
}
</p>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
{
Notifier.Notify += OnNotify;
}
public async Task OnNotify(string key, int value)
{
await InvokeAsync(() =>
{
lastNotification = (key, value);
StateHasChanged();
});
}
private void StartTimer()
{
Timer.Start();
}
public void Dispose()
{
Notifier.Notify -= OnNotify;
}
}
En el ejemplo anterior:
- El temporizador se inicia fuera del contexto de sincronización de Blazor con
_ = Task.Run(Timer.Start). -
NotifierServiceinvoca el métodoOnNotifydel componente.InvokeAsyncse utiliza para cambiar al contexto correcto y poner una repetición de la representación en cola. Para obtener más información, consulta Representación de componentes de Razor de ASP.NET Core. - El componente implementa IDisposable. El elemento delegado
OnNotifyanula su inscripción en el métodoDispose, al que llama el marco cuando se desecha el componente. Para más información, consulte Consumo de componentes ASP.NET CoreRazor.
-
NotifierServiceinvoca el métodoOnNotifydel componente fuera del contexto de sincronización de Blazor.InvokeAsyncse utiliza para cambiar al contexto correcto y poner una repetición de la representación en cola. Para obtener más información, consulta Representación de componentes de Razor de ASP.NET Core. - El componente implementa IDisposable. El elemento delegado
OnNotifyanula su inscripción en el métodoDispose, al que llama el marco cuando se desecha el componente. Para más información, consulte Consumo de componentes ASP.NET CoreRazor.
Importante
Si un componente Razor define un evento que se desencadena desde un subproceso en segundo plano, es posible que sea necesario capturar y restaurar el contexto de ejecución (ExecutionContext) en el momento en que se registra el controlador. Para obtener más información, consulta La llamada a InvokeAsync(StateHasChanged) hace que la página vuelva a la referencia cultural predeterminada (dotnet/aspnetcore #28521).
Para enviar excepciones detectadas desde el objeto TimerService en segundo plano al componente para tratar las excepciones como excepciones de eventos de ciclo de vida normales, consulta la sección Controlar excepciones detectadas fuera de la sección de ciclo de vida de un componente Razor.
Control de las excepciones detectadas fuera del ciclo de vida de un componente Razor
Usa ComponentBase.DispatchExceptionAsync en un componente Razor para procesar las excepciones producidas fuera de la pila de llamadas del ciclo de vida del componente. Esto permite que el código del componente trate las excepciones como si fueran excepciones del método de ciclo de vida. A partir de entonces, los mecanismos de control de errores de Blazor, como los límites de error, pueden procesar las excepciones.
Nota:
ComponentBase.DispatchExceptionAsync se usa en los archivos de los componentes Razor (.razor) que heredan de ComponentBase. Al crear componentes que implement IComponent directly, use RenderHandle.DispatchExceptionAsync.
Para controlar las excepciones detectadas fuera del ciclo de vida de un componente Razor, pase la excepción a DispatchExceptionAsync y espere el resultado:
try
{
...
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
Un escenario común para el enfoque anterior es cuando un componente inicia una operación asincrónica, pero no espera un Task, a menudo denominado patrón fire and forget (dispare y olvídese) porque el método se desencadena (inicia) y el resultado del método se olvida (se elimina). Si se produce un error en la operación, es posible que desees que el componente trate el error como una excepción de ciclo de vida del componente para cualquiera los siguientes objetivos:
- Coloca el componente en un estado defectuoso, por ejemplo, para desencadenar un límite de error.
- Finaliza el circuito si no hay ningún límite de error.
- Desencadena el mismo registro que se produce para las excepciones del ciclo de vida.
En el ejemplo siguiente, el usuario selecciona el botón Enviar informe para desencadenar un método en segundo plano, ReportSender.SendAsync, que envía un informe. En la mayoría de los casos, un componente espera Task de una llamada asincrónica y actualiza la interfaz de usuario para indicar que la operación se completó. En el ejemplo siguiente, el método SendReport no espera a Task y no notifica el resultado al usuario. Dado que el componente descarta intencionadamente Task en SendReport, los errores asincrónicos se producen fuera de la pila de llamadas del ciclo de vida normal, por lo que no se ven en Blazor:
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = ReportSender.SendAsync();
}
}
Para tratar los errores como excepciones del método de ciclo de vida, envía explícitamente las excepciones al componente con DispatchExceptionAsync, como se muestra en el ejemplo siguiente:
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = SendReportAsync();
}
private async Task SendReportAsync()
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
}
}
Un enfoque alternativo aprovecha Task.Run:
private void SendReport()
{
_ = Task.Run(async () =>
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
Para obtener una demostración en funcionamiento, implemente el ejemplo de notificación del temporizador en métodos de componente Invoke externamente para actualizar el estado. En una aplicación Blazor, agrega los siguientes archivos del ejemplo de notificación de temporizador y registra los servicios en el archivo Program como explica la sección:
TimerService.csNotifierService.csNotifications.razor
En el ejemplo se usa un temporizador fuera del ciclo de vida de un componente Razor, donde normalmente no se procesa una excepción no controlada por los mecanismos de control de errores de Blazor, como un límite de error.
En primer lugar, cambia el código de TimerService.cs para crear una excepción artificial fuera del ciclo de vida del componente. En el bucle while de TimerService.cs, inicia una excepción cuando elapsedCount alcance un valor de dos:
if (elapsedCount == 2)
{
throw new Exception("I threw an exception! Somebody help me!");
}
Coloca un límite de error en el diseño principal de la aplicación. Reemplaza el marcado <article>...</article> por el marcado siguiente.
En MainLayout.razor:
<article class="content px-4">
<ErrorBoundary>
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<p class="alert alert-danger" role="alert">
Oh, dear! Oh, my! - George Takei
</p>
</ErrorContent>
</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 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 MainLayout y el resto de los componentes más abajo en la jerarquía de componentes, habilite la representación interactiva para las instancias de los componentes HeadOutlet y Routes en el componente 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 ejecutas la aplicación en este momento, se produce la excepción cuando el recuento transcurrido alcanza un valor de dos. Sin embargo, la interfaz de usuario no cambia. El límite de error no muestra el contenido del error.
Para enviar excepciones desde el servicio de temporizador al componente Notifications, se realizan los siguientes cambios en el componente:
- Inicia el temporizador en una instrucción
try-catch. En la cláusulacatchdel bloquetry-catch, las excepciones se envían de vuelta al componente pasando el Exception a DispatchExceptionAsync y esperando el resultado. - En el método
StartTimer, inicia el servicio de temporizador asincrónico en el Action delegado de Task.Run y descarta intencionadamente el devuelto Task.
El método StartTimer del componente Notifications (Notifications.razor):
private void StartTimer()
{
_ = Task.Run(async () =>
{
try
{
await Timer.Start();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
Cuando el servicio de temporizador se ejecuta y alcanza un recuento de dos, la excepción se envía al componente Razor, que a su vez activa el límite de error para mostrar el contenido de error del <ErrorBoundary> en el componente MainLayout:
Vaya. Santo cielo. - George Takei