在Blazor(Wasm)服务中执行异步初始化逻辑的正确方法是什么?

huangapple go评论77阅读模式
英文:

What is the correct way to do async initialisation logic inside a Blazor (Wasm) Service?

问题

我正在尝试在我的 Blazor Wasm 项目中创建一个 Connection Service 单例。它使用 JS 互操作来监听 onlineoffline 事件,跟踪连接状态并将信息提供给项目中的多个组件。尽管以下部分可以工作,但在构造函数中调用异步函数并丢弃 ValueTask 的做法感觉很不合适。

public class ConnectionService : IAsyncDisposable
{
    private readonly IJSRuntime JsRuntime;
    public bool IsOnline { get; set; } = true;
    public event Action ConnectionStatusChanged;

    public ConnectionService(IJSRuntime jsRuntime)
    {
        JsRuntime = jsRuntime;
        _ = Initialize();
    }

    public ValueTask Initialize()
    {
        return JsRuntime.InvokeVoidAsync("Connection.Initialize", DotNetObjectReference.Create(this));
    }

    [JSInvokable("Connection.StatusChanged")]
    public void OnJsConnectionStatusChanged(bool isOnline)
    {
        if (IsOnline == isOnline) return;
        IsOnline = isOnline;
        ConnectionStatusChanged?.Invoke();
    }

    public async ValueTask DisposeAsync()
    {
        await JsRuntime.InvokeVoidAsync("Connection.Dispose");
    }
}

然而,我在初始化服务时遇到困难。例如,我不能这样做,因为它需要 JSRuntime 来初始化:

var connSingleton = new ConnectionService();
await connSingleton.Initialize();
builder.Services.AddSingleton<ConnectionService>(connSingleton);
英文:

I am trying to create a Connection Service singleton in my Blazor Wasm project. It uses JS Interop to listen for the online and offline events, keep track of connection status and make the information available to multiple components throughout the project. While the following does work, invoking an async function and discarding the ValueTask in the constructor feels very wrong.

public class ConnectionService : IAsyncDisposable
{
	private readonly IJSRuntime JsRuntime;
	public bool IsOnline { get; set; } = true;
	public event Action ConnectionStatusChanged;

	public ConnectionService(IJSRuntime jsRuntime)
	{
		JsRuntime = jsRuntime;
		_ = Initialize();
	}

	public ValueTask Initialize()
	{
		return JsRuntime.InvokeVoidAsync(&quot;Connection.Initialize&quot;, DotNetObjectReference.Create(this));
	}

	[JSInvokable(&quot;Connection.StatusChanged&quot;)]
	public void OnJsConnectionStatusChanged(bool isOnline)
	{
		if (IsOnline == isOnline) return;
		IsOnline = isOnline;
		ConnectionStatusChanged?.Invoke();
	}

	public async ValueTask DisposeAsync()
	{
		await JsRuntime.InvokeVoidAsync(&quot;Connection.Dispose&quot;);
	}
}

However, I'm struggling to find the correct way to initialise the service. I can't do this for example, because it needs the JSRuntime to initialise:

var connSingleton = new ConnectionService();
await connSingleton.Initialize();
builder.Services.AddSingleton&lt;ConnectionService&gt;(connSingleton);

答案1

得分: 1

不要这样做:

 _ = Initialize();

在没有处理可能发生的异常的情况下。

现在任务没有所有者,我认为任何运行时异常都会传播到应用程序,并可能导致崩溃。

问题在于构造函数是同步的:你不能在同步上下文中运行异步代码。

正确的方法是将任务分配给类的变量/属性。然后在尝试使用它检查任务是否已完成,并获取它检索的任何数据。

在此上下文中使用 Task,而不是 ValueTaskValueTask 不设计为可重入。

这里是使用 WeatherForecastService 进行通用演示,其中服务在其构造函数中获取数据。

public class WeatherForecastService
{
    private List<WeatherForecast>? _weatherForecasts;

    // 公共只读,以便我们可以等待它或在需要时外部检查加载状态。
    public Task Loading { get; private set; } = Task.CompletedTask;

    public WeatherForecastService()
    {
        // 将任务分配给 Loading,我们可以在同步代码中执行此操作
        Loading = GetWeatherForecastsAsync();
    }

    public async ValueTask<IEnumerable<WeatherForecast>> GetForecastsAsync()
    {
        // 仅在任务仍在运行时等待
        // 如果它是已完成的任务,我们就继续执行
        await Loading;
        return _weatherForecasts ?? Enumerable.Empty<WeatherForecast>();
    }

    private async Task GetWeatherForecastsAsync()
    {
        // 模拟异步过程
        await Task.Delay(1000);

        DateOnly startDate = DateOnly.FromDateTime(DateTime.Now);

        _weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToList();
    }

    private static readonly string[] Summaries = new[]
    { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
}

更改 FetchData

@code {
    private IEnumerable<WeatherForecast>? forecasts;

    protected override async Task OnInitializedAsync()
        => forecasts = await ForecastService.GetForecastsAsync();
}
英文:

You shouldn't do this:

 _ = Initialize();

Without handling the exceptions that may occur.

There's now no owner of the task, and I believe any runtime exceptions will propagate all the way up to the application and may crash it.

The problem is constructors are synchronous: you can't run async code within a synchronous context.

The way to do this is assign the Task to a class variable/property. Then check the Task is complete before trying to use whatever data it is retrieving.

Use a Task, not a ValueTask in this context. ValueTask is not designed to be re-entrant.

Here's a generic demonstration using the WeatherForecastService where the service gets the data in it's constructor.

public class WeatherForecastService
{
    private List&lt;WeatherForecast&gt;? _weatherForecasts;

    // Public read only so we can await it or check the load status externally if we wish.
    public Task Loading { get; private set; } = Task.CompletedTask;

    public WeatherForecastService()
    {
        // assign the Task to Loading which we can do in sync code
        Loading = GetWeatherForecastsAsync();
    }

    public async ValueTask&lt;IEnumerable&lt;WeatherForecast&gt;&gt; GetForecastsAsync()
    {
        // We only await if the task is still running.
        // If it&#39;s a completed task we just waltz on through
        await Loading;
        return _weatherForecasts ?? Enumerable.Empty&lt;WeatherForecast&gt;();
    }

    private async Task GetWeatherForecastsAsync()
    {
        // Fake an async process
        await Task.Delay(1000);

        DateOnly startDate = DateOnly.FromDateTime(DateTime.Now);

        _weatherForecasts = Enumerable.Range(1, 5).Select(index =&gt; new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToList();
    }

    private static readonly string[] Summaries = new[]
    { &quot;Freezing&quot;, &quot;Bracing&quot;, &quot;Chilly&quot;, &quot;Cool&quot;, &quot;Mild&quot;, &quot;Warm&quot;, &quot;Balmy&quot;, &quot;Hot&quot;, &quot;Sweltering&quot;, &quot;Scorching&quot; };
}

Change FetchData

@code {
    private IEnumerable&lt;WeatherForecast&gt;? forecasts;

    protected override async Task OnInitializedAsync()
        =&gt; forecasts = await ForecastService.GetForecastsAsync();
}

答案2

得分: 0

在你的Program.cs中,处理这个问题的一种方法是对其进行小的更改:

在你的Program.cs中:
将默认的下面一行代码(通常是代码的最后一行)更改为:

var host = builder.Build();

// 现在获取需要在应用程序加载之前进行异步初始化的任何服务
// 并进行初始化
// 这里的任务将阻止页面渲染,直到完成
var connSingleton = host.Services.GetRequiredService&lt;ConnectionService&gt;();
await connSingleton.Initialize();

// 现在我们可以调用RunAsync来渲染第一个页面组件
await host.RunAsync();

这是我在处理需要在应用程序加载之前进行异步初始化的Blazor WASM服务时使用的简化版本。请注意长时间运行的初始化任务。

英文:

One way to handle it is to make a small change in your Program.cs.

In your Program.cs:
Change the default below line (usually the last line of code)

await builder.Build().RunAsync();

To

var host = builder.Build();

// Now get any services that need async initialization before app loads 
// and Init them
// Tasks here will prevent page rendering until completed
var connSingleton = host.Services.GetRequiredService&lt;ConnectionService&gt;();
await connSingleton.Initialize();

// Now we can call RunAsync to render the first page component
await host.RunAsync();

This is a simplified version of how I do it for my Blazor WASM services that need to be initialized asynchronously before the app loads. Be cautious of long running Initialization tasks.

huangapple
  • 本文由 发表于 2023年7月11日 12:15:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76658678.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定