Contexto de sincronização do Blazor ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Blazor usa um contexto de sincronização (SynchronizationContext) para impor um thread lógico de execução. Os métodos de ciclo de vida e os retornos de chamada de evento de componente gerados por Blazor são executados no contexto de sincronização.

BlazorO contexto de sincronização do lado do servidor tenta emular um ambiente de thread único para que ele corresponda ao modelo WebAssembly no navegador, que é de thread único. Essa emulação tem escopo apenas para um circuito individual, o que significa que dois circuitos diferentes podem funcionar em paralelo. Em qualquer dado momento dentro de um circuito, o trabalho é executado exatamente em um thread, o que gera a impressão de um só thread lógico. Duas operações não são executadas simultaneamente dentro do mesmo circuito.

Evitar chamadas de bloqueio de thread

Geralmente, não chame os métodos a seguir nos componentes. Os seguintes métodos bloqueiam o thread de execução e, portanto, impedem que o aplicativo retome o trabalho até que o Task subjacente seja concluído:

Observação

Exemplos de documentação de Blazor que usam os métodos de bloqueio de thread mencionados nesta seção estão usando os métodos apenas para fins de demonstração, não como diretrizes de codificação recomendadas. Por exemplo, algumas demonstrações de código de componente simulam um processo de execução longa chamando Thread.Sleep.

Invocar métodos de componente externamente para atualizar o estado

Caso um componente precise ser atualizado com base em um evento externo, como um temporizador ou outra notificação, use o método InvokeAsync, que envia a execução de código para o contexto de sincronização de Blazor. Por exemplo, considere o serviço de notificador a seguir, que pode notificar qualquer componente de escuta sobre o estado atualizado. O método Update pode ser chamado de qualquer lugar do aplicativo.

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 os serviços:

  • Para desenvolvimento do lado do cliente, registre os serviços como singletons no arquivo Program do lado do cliente:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Para desenvolvimento no lado do servidor, registre os serviços conforme escopo no arquivo Program do servidor:

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

Use o NotifierService para atualizar um 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;
    }
}

No exemplo anterior:

Importante

Se um componente Razor definir um evento disparado de um thread em segundo plano, o componente poderá ser necessário para capturar e restaurar o contexto de execução (ExecutionContext) no momento em que o manipulador for registrado. Para obter mais informações, consulte A chamada InvokeAsync(StateHasChanged) faz com que a página faça fallback para a cultura padrão (dotnet/aspnetcore #28521).

Para expedir as exceções capturadas do TimerService em segundo plano para o componente, a fim de tratar as exceções como exceções normais de evento de ciclo de vida, confira a seção Tratar exceções capturadas fora do ciclo de vida de um componente do Razor.

Manipular exceções capturadas fora do ciclo de vida de um componente Razor

Use ComponentBase.DispatchExceptionAsync em um componente Razor para processar exceções geradas fora da pilha de chamadas do ciclo de vida do componente. Isso permite que o código do componente trate exceções como se fossem exceções de método de ciclo de vida. Depois disso, os mecanismos de tratamento de erros do Blazor, como limites de erro, podem processar as exceções.

Observação

ComponentBase.DispatchExceptionAsync é usado em arquivos de componente do Razor (.razor) que herdam de ComponentBase. Ao criar componentes que implement IComponent directly, use RenderHandle.DispatchExceptionAsync.

Para lidar com exceções capturadas fora do ciclo de vida de um componente Razor, passe a exceção para DispatchExceptionAsync e aguarde o resultado:

try
{
    ...
}
catch (Exception ex)
{
    await DispatchExceptionAsync(ex);
}

Um cenário comum para a abordagem anterior é quando um componente inicia uma operação assíncrona, mas não aguarda um Task, o que é frequentemente chamado de padrão de acionar e esquecer, porque o método é acionado (iniciado) e o resultado do método é esquecido (jogado fora). Se a operação falhar, talvez você queira que o componente trate a falha como uma exceção do ciclo de vida do componente para qualquer uma das seguintes metas:

  • Coloque o componente em um estado com falha, por exemplo, para disparar um limite de erro.
  • Encerre o circuito se não houver limite de erro.
  • Dispare o mesmo registro em log que ocorre para exceções de ciclo de vida.

No exemplo a seguir, o usuário seleciona o botão Enviar relatório para disparar um método em segundo plano, ReportSender.SendAsync, que envia um relatório. Na maioria dos casos, um componente aguarda a Task de uma chamada assíncrona e atualiza a interface do usuário para indicar a operação concluída. No exemplo a seguir, o método SendReport não aguarda um Task e não relata o resultado para o usuário. Como o componente descarta intencionalmente a Task no SendReport, quaisquer falhas assíncronas ocorrem fora da pilha de chamadas do ciclo de vida normal, portanto, não são vistas por Blazor:

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = ReportSender.SendAsync();
    }
}

Para tratar falhas como exceções de método de ciclo de vida, envie explicitamente exceções de volta para o componente com DispatchExceptionAsync, como o exemplo a seguir demonstra:

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

Uma abordagem alternativa aproveita Task.Run:

private void SendReport()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Para uma demonstração funcional, implemente o exemplo de notificação de temporizador em Invocar métodos de componente externamente para atualizar o estado. Em um aplicativo Blazor, adicione os seguintes arquivos do exemplo de notificação do temporizador e registre os serviços no arquivo Program, como explica a seção:

  • TimerService.cs
  • NotifierService.cs
  • Notifications.razor

O exemplo usa um temporizador fora do ciclo de vida de um componente Razor, em que uma exceção sem tratamento normalmente não é processada por Blazormecanismos de tratamento de erros, como um limite de erro.

Primeiro, altere o código TimerService.cs para criar uma exceção artificial fora do ciclo de vida do componente. No loop while de TimerService.cs, gere uma exceção quando elapsedCount atingir um valor de dois:

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

Coloque um limite de erro no layout principal do aplicativo. Substitua a marcação <article>...</article> pela seguinte marcação.

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

Em aplicativos Web Blazor com o limite de erro aplicado apenas a um componente MainLayout estático, o limite só fica ativo durante a fase de renderização estática do lado do servidor (SSR estática). O limite não é ativado apenas porque um componente mais abaixo da hierarquia de componentes é interativo. Para habilitar a interatividade de forma ampla para o componente MainLayout e o restante dos componentes mais abaixo na hierarquia de componentes, habilite a renderização interativa para as instâncias dos componentes HeadOutlet e Routes no componente App (Components/App.razor). O exemplo a seguir adota o modo de renderização do Servidor Interativo (InteractiveServer):

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Se você executar o aplicativo nesse ponto, a exceção será gerada quando a contagem decorrida atingir um valor de dois. No entanto, a interface do usuário não é alterada. O limite de erro não mostra o conteúdo do erro.

Para expedir exceções do serviço de temporizador de volta para o componente Notifications, as seguintes alterações são feitas no componente:

  • Inicie o temporizador em uma instrução try-catch. Na cláusula catch do bloco try-catch, as exceções são enviadas de volta para o componente passando o Exception para DispatchExceptionAsync e aguardando o resultado.
  • No método StartTimer, inicie o serviço de temporizador assíncrono no delegado Action de Task.Run e descarte intencionalmente o Task retornado.

O método StartTimer do componente Notifications (Notifications.razor):

private void StartTimer()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await Timer.Start();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Quando o serviço de temporizador é executado e atinge uma contagem de dois, a exceção é expedida para o componente Razor, que, por sua vez, dispara o limite de erro para exibir o conteúdo de erro do <ErrorBoundary> no componente MainLayout:

Ah, querida! Ah, meu Deus! - George Takei