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 .NET 8 de este artículo.

Importante

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

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

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 llame 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, use 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);
    }
}
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;
}
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;
}

Registre los servicios:

  • Para el desarrollo del lado cliente, registre 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, registre los servicios como con ámbito en el archivo Program del lado servidor:

    builder.Services.AddScoped<NotifierService>();
    builder.Services.AddScoped<TimerService>();
    

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

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:

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 más información, consulte 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 TimerService en segundo plano al componente para tratar las excepciones como excepciones de eventos de ciclo de vida normales, consulte 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

Use 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 desee que el componente trate el error como una excepción de ciclo de vida del componente para cualquiera los siguientes objetivos:

  • Coloque el componente en un estado defectuoso, por ejemplo, para desencadenar un límite de error.
  • Finalice el circuito si no hay ningún límite de error.
  • Desencadene 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íe 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, agregue los siguientes archivos del ejemplo de notificación de temporizador y registre 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, cambie 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, inicie una excepción cuando elapsedCount alcance un valor de dos:

if (elapsedCount == 2)
{
    throw new Exception("I threw an exception! Somebody help me!");
}

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

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Si ejecuta 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:

  • Inicie el temporizador en una instruccióntry-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, inicie el servicio de temporizador asincrónico en el Action delegado de Task.Run e descarte 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