英文:
What is the correct way to do async initialisation logic inside a Blazor (Wasm) Service?
问题
我正在尝试在我的 Blazor Wasm 项目中创建一个 Connection Service 单例。它使用 JS 互操作来监听 online
和 offline
事件,跟踪连接状态并将信息提供给项目中的多个组件。尽管以下部分可以工作,但在构造函数中调用异步函数并丢弃 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("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");
}
}
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<ConnectionService>(connSingleton);
答案1
得分: 1
不要这样做:
_ = Initialize();
在没有处理可能发生的异常的情况下。
现在任务没有所有者,我认为任何运行时异常都会传播到应用程序,并可能导致崩溃。
问题在于构造函数是同步的:你不能在同步上下文中运行异步代码。
正确的方法是将任务分配给类的变量/属性。然后在尝试使用它检查任务是否已完成,并获取它检索的任何数据。
在此上下文中使用 Task
,而不是 ValueTask
。ValueTask
不设计为可重入。
这里是使用 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<WeatherForecast>? _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<IEnumerable<WeatherForecast>> GetForecastsAsync()
{
// We only await if the task is still running.
// If it's a completed task we just waltz on through
await Loading;
return _weatherForecasts ?? Enumerable.Empty<WeatherForecast>();
}
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 => 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" };
}
Change FetchData
@code {
private IEnumerable<WeatherForecast>? forecasts;
protected override async Task OnInitializedAsync()
=> forecasts = await ForecastService.GetForecastsAsync();
}
答案2
得分: 0
在你的Program.cs中,处理这个问题的一种方法是对其进行小的更改:
在你的Program.cs中:
将默认的下面一行代码(通常是代码的最后一行)更改为:
var host = builder.Build();
// 现在获取需要在应用程序加载之前进行异步初始化的任何服务
// 并进行初始化
// 这里的任务将阻止页面渲染,直到完成
var connSingleton = host.Services.GetRequiredService<ConnectionService>();
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<ConnectionService>();
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论