Compartir vía


Contexto de sincronización de ASP.NET Core Blazor

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 9 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, 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.

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 readonly static 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);
    }
}
namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private readonly static 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 readonly static 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}");
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly static 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}");
                }
            }
        }
    }

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

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

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

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

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).
  • NotifierService invoca el método OnNotify del componente. InvokeAsync se 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 OnNotify anula su inscripción en el método Dispose, al que llama el marco cuando se desecha el componente. Para obtener más información, consulta Ciclo de vida de componentes Razor de ASP.NET Core.

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.cs
  • NotifierService.cs
  • Notifications.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 de MainLayout y el rest de los componentes más abajo de la jerarquía de componentes, habilita la representación interactiva para las instancias de componente 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áusula catch del bloque try-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