Delen via


synchronisatiecontext ASP.NET Core Blazor

Notitie

Dit is niet de nieuwste versie van dit artikel. Zie de .NET 9-versie van dit artikelvoor de huidige release.

Waarschuwing

Deze versie van ASP.NET Core wordt niet meer ondersteund. Zie de .NET- en .NET Core-ondersteuningsbeleidvoor meer informatie. Zie de .NET 9-versie van dit artikelvoor de huidige release.

Belangrijk

Deze informatie heeft betrekking op een pre-releaseproduct dat aanzienlijk kan worden gewijzigd voordat het commercieel wordt uitgebracht. Microsoft geeft geen garanties, uitdrukkelijk of impliciet, met betrekking tot de informatie die hier wordt verstrekt.

Zie de .NET 9-versie van dit artikelvoor de huidige release.

Blazor gebruikt een synchronisatiecontext (SynchronizationContext) om één logische thread van uitvoering af te dwingen. De levenscyclusmethoden van een onderdeel en callbacks van gebeurtenissen die door Blazor worden gegenereerd, worden uitgevoerd in de synchronisatiecontext.

Blazorsynchronisatiecontext aan de serverzijde probeert een omgeving met één thread te emuleren, zodat deze nauw overeenkomt met het WebAssembly-model in de browser, dat één thread bevat. Deze emulatie is alleen gericht op een afzonderlijk circuit, wat betekent dat twee verschillende circuits parallel kunnen worden uitgevoerd. Op een bepaald tijdstip binnen een circuit wordt werk uitgevoerd op precies één thread, wat de indruk van één logische thread oplevert. Er worden geen twee bewerkingen tegelijk uitgevoerd binnen hetzelfde circuit.

Thread-blokkerende aanroepen voorkomen

In het algemeen roept u de volgende methoden niet aan in onderdelen. Met de volgende methoden wordt de uitvoeringsthread geblokkeerd en kan de app dus geen werk hervatten totdat de onderliggende Task is voltooid:

Notitie

Blazor documentatievoorbeelden die gebruikmaken van de thread-blokkerende methoden zoals vermeld in deze sectie, worden alleen gebruikt voor demonstratiedoeleinden en niet voor het geven van aanbevolen coderingsrichtlijnen. Een paar demonstraties van onderdeelcode simuleren bijvoorbeeld een langlopend proces door Thread.Sleepaan te roepen.

Componentmethoden extern aanroepen om de status bij te werken

In het geval dat een onderdeel moet worden bijgewerkt op basis van een externe gebeurtenis, zoals een timer of een andere melding, gebruikt u de methode InvokeAsync, waarmee de uitvoering van code wordt verzonden naar Blazorsynchronisatiecontext. Denk bijvoorbeeld aan de volgende notifier-service die elk luisterend onderdeel kan informeren over de bijgewerkte status. De methode Update kan overal in de app worden aangeroepen.

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

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new Timer();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}

NotifierService.cs:

namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}

Registreer de diensten.

  • Registreer voor ontwikkeling aan de clientzijde de services als singletons in het Program-bestand aan de clientzijde:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Registreer voor ontwikkeling aan de serverzijde de services als bereik in het Program-bestand van de server:

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

Gebruik de NotifierService om een onderdeel bij te werken.

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

In het voorgaande voorbeeld:

  • De timer wordt gestart buiten de synchronisatiecontext van Blazormet _ = Task.Run(Timer.Start).
  • NotifierService roept de OnNotify methode van het onderdeel aan. InvokeAsync wordt gebruikt om over te schakelen naar de juiste context en een rerender in de wachtrij te zetten. Zie ASP.NET Core Razor component renderingvoor meer informatie.
  • Het onderdeel implementeert IDisposable. De OnNotify gedelegeerde wordt afgemeld bij de methode Dispose, die wordt aangeroepen door het framework wanneer het onderdeel wordt verwijderd. Zie ASP.NET Core Razor levenscyclus van onderdelen voor meer informatie.

Belangrijk

Als een Razor-onderdeel een gebeurtenis definieert die wordt geactiveerd vanuit een achtergrondthread, kan het onderdeel vereist zijn om de uitvoeringscontext (ExecutionContext) vast te leggen en te herstellen op het moment dat de handler is geregistreerd. Zie Bellen InvokeAsync(StateHasChanged) ervoor zorgt dat pagina terugvalt op de standaardcultuur (dotnet/aspnetcore #28521)voor meer informatie.

Als u gevangen uitzonderingen vanuit de achtergrond TimerService naar het onderdeel wilt doorsturen om de uitzonderingen zoals normale levenscyclusevenement-uitzonderingen te behandelen, raadpleegt u de sectie Gevangen uitzonderingen verwerken buiten de levenscyclus van een Razor onderdeel.

Onderschepte uitzonderingen buiten de levenscyclus van een Razor-onderdeel verwerken

Gebruik ComponentBase.DispatchExceptionAsync in een Razor-onderdeel om uitzonderingen te verwerken die buiten de levenscyclusoproepstack van het onderdeel zijn opgetreden. Hierdoor kan de code van het onderdeel uitzonderingen behandelen alsof het uitzonderingen voor de levenscyclusmethode zijn. Daarna kunnen Blazorfoutafhandelingsmechanismen, zoals foutgrenzen, de uitzonderingen verwerken.

Notitie

ComponentBase.DispatchExceptionAsync wordt gebruikt in Razor onderdeelbestanden (.razor) die overnemen van ComponentBase. Wanneer u onderdelen maakt die implement IComponent directly, gebruikt u RenderHandle.DispatchExceptionAsync.

Als u gevangen uitzonderingen buiten de levenscyclus van een Razor-onderdeel wilt afhandelen, geeft u de uitzondering door aan DispatchExceptionAsync en wacht u op het resultaat:

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

Een veelvoorkomend scenario voor de voorgaande benadering is wanneer een onderdeel een asynchrone bewerking start, maar niet wacht op een Task, ook wel het brand genoemd en vergeet patroon omdat de methode wordt geactiveerd (gestart) en het resultaat van de methode wordt vergeten (weggegooid). Als de bewerking mislukt, wilt u mogelijk dat het onderdeel de fout behandelt als een levenscyclusuitzondering voor onderdelen voor een van de volgende doelen:

  • Plaats het onderdeel in een fouttoestand, bijvoorbeeld om een foutgrens te activeren.
  • Beëindig het circuit als er geen foutgrens is.
  • Activeer dezelfde logboekregistratie die optreedt voor levenscyclus-uitzonderingen.

In het volgende voorbeeld selecteert de gebruiker de knop Rapport verzenden om een achtergrondmethode te activeren, ReportSender.SendAsync, waarmee een rapport wordt verzonden. In de meeste gevallen wacht een onderdeel op de Task van een asynchrone aanroep en werkt de gebruikersinterface bij om aan te geven dat de bewerking is voltooid. In het volgende voorbeeld wacht de SendReport methode niet op een Task en rapporteert het resultaat niet aan de gebruiker. Omdat het onderdeel de Task opzettelijk weglaat in SendReport, vinden eventuele asynchrone fouten buiten de normale levenscyclusaanroepstack plaats, waardoor ze niet worden opgemerkt door Blazor:

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

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

Als u fouten zoals uitzonderingen voor de levenscyclusmethode wilt behandelen, verzendt u expliciet uitzonderingen terug naar het onderdeel met DispatchExceptionAsync, zoals in het volgende voorbeeld wordt getoond:

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

Een alternatieve benadering maakt gebruik van Task.Run:

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

Voor een werkende demonstratie implementeert u het voorbeeld van de timermelding in Componentmethoden extern aanroepen om de statusbij te werken. Voeg in een Blazor-app de volgende bestanden toe uit het voorbeeld van de timermelding en registreer de services in het Program-bestand, zoals in de sectie wordt uitgelegd:

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

In het voorbeeld wordt een timer buiten de levenscyclus van een Razor onderdeel gebruikt, waarbij een niet-verwerkte uitzondering normaal gesproken niet wordt verwerkt door de mechanismen voor foutafhandeling van Blazor, zoals een foutgrens.

Wijzig eerst de code in TimerService.cs om een kunstmatige uitzondering te maken buiten de levenscyclus van het onderdeel. In de while lus van TimerService.csgenereert u een uitzondering wanneer de elapsedCount een waarde van twee bereikt:

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

Plaats een foutgrens in de hoofdindeling van de app. Vervang de <article>...</article> markering door de volgende markeringen.

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 App, waarbij de foutgrens alleen wordt toegepast op een statische MainLayout-component, is de grens alleen actief tijdens de statische server-side rendering (SSR) fase. De grens wordt niet geactiveerd omdat een onderdeel verderop in de onderdeelhiërarchie interactief is. Als u interactiviteit breed wilt inschakelen voor het MainLayout-onderdeel en de rest van de onderdelen, schakelt u interactieve rendering in voor de HeadOutlet- en Routes onderdeelexemplaren in het App-onderdeel (Components/App.razor). In het volgende voorbeeld wordt de rendermodus Interactive Server (InteractiveServer) gebruikt:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Als u de app op dit punt uitvoert, wordt de uitzondering opgeworpen wanneer het verstreken aantal een waarde van twee bereikt. De gebruikersinterface verandert echter niet. De foutgrens geeft de foutinhoud niet weer.

Als u uitzonderingen van de timerservice wilt verzenden naar het Notifications onderdeel, worden de volgende wijzigingen aangebracht in het onderdeel:

  • Start de timer in een try-catch instructie. In de catch component van het try-catch blok worden uitzonderingen teruggestuurd naar het onderdeel door de Exception door te geven aan DispatchExceptionAsync en te wachten op het resultaat.
  • Start in de StartTimer methode de asynchrone timerservice in de Action delegate van Task.Run en negeer de geretourneerde Taskopzettelijk.

De StartTimer methode van het Notifications onderdeel (Notifications.razor):

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

Wanneer de timerservice een aantal van twee uitvoert en bereikt, wordt de uitzondering verzonden naar het Razor onderdeel, waardoor de foutgrens wordt geactiveerd om de foutinhoud van de <ErrorBoundary> weer te geven in het MainLayout-onderdeel:

Oh, lieve! Oh, mijn! - George Takei