ASP.NET Core Blazor-Synchronisierungskontext

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Blazor verwendet einen Synchronisierungskontext (SynchronizationContext), um einen einzelnen logischen Ausführungsthread zu erzwingen. Die Lebenszyklusmethoden einer Komponente und Ereignisrückrufe, die von Blazor ausgelöst werden, werden in diesem Synchronisierungskontext ausgeführt.

Der serverseitige Synchronisierungskontext von Blazor versucht, eine Single-Thread-Umgebung zu emulieren, so dass er dem WebAssembly-Modell im Browser, das Single-Thread ist, sehr nahe kommt. Diese Emulation gilt nur für einen einzelnen Schaltkreis, was bedeutet, dass zwei verschiedene Schaltkreise parallel ausgeführt werden können. Zu jedem Zeitpunkt wird die Arbeit in einem Schaltkreis für genau einen Thread ausgeführt, woraus der Eindruck eines einzelnen logischen Threads entsteht. Es werden keine zwei Vorgänge gleichzeitig im gleichen Schaltkreis ausgeführt.

Vermeiden Sie eine Threadblockierung von Aufrufen.

Rufen Sie generell in Komponenten nicht die folgenden Methoden auf. Die folgenden Methoden blockieren die Threadausführung und hindern somit die App an der Wiederaufnahme der Arbeit, bis der zugrunde liegende Task beendet ist:

Hinweis

In den Beispielen in der Blazor-Dokumentation werden die in diesem Abschnitt erwähnten Threadblockierungsmethoden nur zu Demonstrationszwecken und nicht als Codierungsempfehlung verwendet. Beispielsweise simulieren einige Komponentencodedemonstrationen einen Prozess mit langer Ausführungsdauer, indem Thread.Sleep aufgerufen wird.

Externes Aufrufen von Komponentenmethoden zur Aktualisierung des Status

Wenn eine Komponente aufgrund eines externen Ereignisses (z. B. eines Timers oder anderer Benachrichtigungen) aktualisiert werden muss, verwenden Sie die InvokeAsync-Methode, mit der die Codeausführung an den Synchronisierungskontext von Blazor weitergeleitet wird. Betrachten Sie beispielsweise den folgenden Benachrichtigungsdienst, der jede lauschende Komponente über den aktualisierten Zustand benachrichtigen kann. Die Update-Methode kann überall in der App aufgerufen werden.

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

Registrieren des Diensts:

  • Für die clientseitige Entwicklung registrieren Sie die Dienste als Singletons in der clientseitigen Datei Program:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Für die serverseitige Entwicklung registrieren Sie die Dienste als Bereich in der Server-Program-Datei:

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

Verwenden Sie NotifierService, um eine Komponente zu aktualisieren.

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

Im vorherigen Beispiel:

  • Der Timer wird außerhalb des Synchronisierungskontexts von Blazor mit _ = Task.Run(Timer.Start) initiiert.
  • NotifierService ruft die Methode OnNotify der Komponente auf. InvokeAsync wird verwendet, um zum richtigen Kontext zu wechseln und ein Rendering in die Warteschlange zu stellen. Weitere Informationen finden Sie unter Rendering von Razor-Komponenten in ASP.NET Core.
  • Die Komponente implementiert IDisposable. Das Abonnement des OnNotify-Delegaten wird in der Dispose-Methode gekündigt. Diese wird vom Framework aufgerufen, wenn die Komponente verworfen wird. Weitere Informationen finden Sie unter Rendering von Razor-Komponenten in ASP.NET Core.
  • NotifierService ruft die OnNotify-Methode der Komponente außerhalb des Synchronisierungskontexts von Blazor auf. InvokeAsync wird verwendet, um zum richtigen Kontext zu wechseln und ein Rendering in die Warteschlange zu stellen. Weitere Informationen finden Sie unter Rendering von Razor-Komponenten in ASP.NET Core.
  • Die Komponente implementiert IDisposable. Das Abonnement des OnNotify-Delegaten wird in der Dispose-Methode gekündigt. Diese wird vom Framework aufgerufen, wenn die Komponente verworfen wird. Weitere Informationen finden Sie unter Rendering von Razor-Komponenten in ASP.NET Core.

Wichtig

Wenn eine Razor-Komponente ein Ereignis definiert, das von einem Hintergrundthread ausgelöst wird, ist die Komponente möglicherweise erforderlich, um den Ausführungskontext (ExecutionContext) zum Zeitpunkt der Registrierung des Handlers zu erfassen und wiederherzustellen. Weitere Informationen finden Sie unter Aufrufen von InvokeAsync(StateHasChanged) führt zu einem Fallback der Seite zur Standardkultur (dotnet/aspnetcore #28521).

Informationen zum Senden von abgefangenen Ausnahmen aus dem hintergründigen TimerService an die Komponente, um die Ausnahmen wie normale Lebenszyklusereignis-Ausnahmen zu behandeln, finden Sie im Abschnitt Behandeln von abgefangenen Ausnahmen außerhalb des Lebenszyklus einer Razor-Komponente.

Behandeln abgefangener Ausnahmen außerhalb des Lebenszyklus einer Razor-Komponente

Verwenden Sie ComponentBase.DispatchExceptionAsync in einer Razor-Komponente, um Ausnahmen zu verarbeiten, die außerhalb der Lebenszyklus-Aufrufliste der Komponente ausgelöst werden. Dadurch kann der Code der Komponente Ausnahmen so behandeln, als ob es sich um Lebenszyklusmethoden-Ausnahmen handelt. Anschließend können Blazor-Fehlerbehandlungsmechanismen, z. B Fehlerbegrenzungen, die Ausnahmen verarbeiten.

Hinweis

ComponentBase.DispatchExceptionAsync wird in Razor-Komponentendateien (.razor) verwendet, die von ComponentBase erben. Verwenden Sie RenderHandle.DispatchExceptionAsync beim Erstellen von Komponenten, die implement IComponent directly.

Um abgefangene Ausnahmen außerhalb des Lebenszyklus einer Razor-Komponente zu behandeln, übergeben Sie die Ausnahme an DispatchExceptionAsync , und warten Sie auf das Ergebnis:

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

Ein häufiges Szenario für obigen Ansatz besteht darin, dass eine Komponente einen asynchronen Vorgang startet, aber nicht auf eine Task wartet. Dieses Muster wird häufig als Fire and Forget (Auslösen und Vergessen) bezeichnet, da die Methode ausgelöst (Fire) und das Ergebnis der Methode vergessen (Forget) wird. Wenn bei dem Vorgang ein Fehler auftritt, möchten Sie möglicherweise dennoch, dass die Komponente den Fehler als Ausnahme im Komponentenlebenszyklus für die folgenden Ziele behandelt:

  • Versetzen Sie die Komponente in einen fehlerhaften Zustand, z. B. um eine Fehlerbegrenzung auszulösen.
  • Trennen Sie die Leitung, wenn keine Fehlergrenze vorhanden ist.
  • Lösen Sie die gleiche Protokollierung aus, die für Lebenszyklusausnahmen erfolgt.

Im folgenden Beispiel wählen Benutzer*innen die Schaltfläche Bericht senden aus, um die Hintergrundmethode ReportSender.SendAsync auszulösen, die einen Bericht sendet. In den meisten Fällen erwartet eine Komponente Task eines asynchronen Aufrufs und aktualisiert die Benutzeroberfläche, um anzugeben, dass der Vorgang abgeschlossen wurde. Im folgenden Beispiel erwartet die SendReport-Methode kein Task und meldet das Ergebnis nicht an die Benutzer*innen. Da die Komponente absichtlich Task in SendReportverwirft, treten alle asynchronen Fehler außerhalb der normalen Lebenszyklus-Aufrufliste auf. Daher werden sie von Blazor nicht angezeigt:

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

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

Um Fehler wie Lebenszyklusmethoden-Ausnahmen zu behandeln, senden Sie Ausnahmen explizit zurück an die Komponente mit DispatchExceptionAsync, wie im folgenden Beispiel veranschaulicht:

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

Bei einem alternativen Ansatz wird Task.Run verwendet:

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

Für eine funktionierende Demonstration implementieren Sie das Beispiel für Zeitgeberbenachrichtigungen in Externes Aufrufen von Komponentenmethoden zur Aktualisierung des Status. Fügen Sie in einer Blazor-App die folgenden Dateien aus dem Beispiel für Zeitgeberbenachrichtigungen hinzu, und registrieren Sie die Dienste in der Program-Datei, wie im Abschnitt erläutert:

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

Das Beispiel verwendet einen Timer außerhalb des Lebenszyklus einer Razor-Komponente, bei dem eine nicht behandelte Ausnahme normalerweise nicht von den Blazor-Fehlerbehandlungsmechanismen, z. B. von Fehlerbegrenzung, verarbeitet wird.

Ändern Sie zunächst den Code in TimerService.cs, um eine künstliche Ausnahme außerhalb des Lebenszyklus der Komponente zu erzeugen. Lösen Sie in der while-Schleife von TimerService.cs eine Ausnahme aus, wenn elapsedCount einen Wert von zwei erreicht:

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

Platzieren Sie eine Fehlerbegrenzung im Standard Layout der App. Ersetzen Sie das <article>...</article>-Markup durch folgendes Markup.

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

In Blazor-Web-Apps, bei denen die Fehlergrenze nur auf eine statische MainLayout-Komponente angewendet wird, ist die Grenze nur während des statischen serverseitigen Renderings (statisches SSR) aktiv. Die Grenze wird nicht aktiviert, nur weil eine Komponente weiter unten in der Komponentenhierarchie interaktiv ist. Um die Interaktivität für die Komponente MainLayout und die übrigen Komponenten weiter unten in der Komponentenhierarchie weitgehend zu aktivieren, aktivieren Sie interaktives Rendering für die Komponenteninstanzen HeadOutlet und Routes in der Komponente App (Components/App.razor). Im folgenden Beispiel wird der interaktive Serverrendermodus (InteractiveServer) verwendet:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Wenn Sie die App an diesem Punkt ausführen, wird die Ausnahme ausgelöst, wenn die verstrichene Anzahl einen Wert von zwei erreicht. Die Benutzeroberfläche ändert sich jedoch nicht. Die Fehlerbegrenzung zeigt den Fehlerinhalt nicht an.

Um Ausnahmen vom Timerdienst zurück an die Notifications-Komponente zu übergeben, werden die folgenden Änderungen an der Komponente vorgenommen:

  • Starten Sie den Timer in einer try-catch-Anweisung. In der catch-Klausel des try-catch-Blocks werden Ausnahmen an die Komponente zurückgesendet, indem die Exception an DispatchExceptionAsync übergeben und auf das Ergebnis gewartet wird.
  • Starten Sie in der StartTimer-Methode den asynchronen Timerdienst im Action-Delegaten von Task.Run, und verwerfen Sie die zurückgegebene Task.

In der StartTimer-Methode der Notifications-Komponente (Notifications.razor):

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

Wenn der Zeitgeberdienst ausgeführt wird und die Anzahl 2 erreicht, wird die Ausnahme an die Razor-Komponente weitergegeben, wodurch wiederum die Fehlergrenze ausgelöst wird, um den Fehlerinhalt von <ErrorBoundary> in der MainLayout-Komponente anzuzeigen:

Ach, du liebe Zeit! Oh, Mann! - George Takei