.NET6 Blazor WebAssembly OnInitializedAsync初始化单例服务子页面取值异常
问题根因
.NET 6 版本 Blazor WebAssembly 采用子组件优先的渲染逻辑:根组件App.razor的OnInitializedAsync方法执行到第一个await关键字时,会立即将控制权交回给渲染管线,此时还未等待所有异步Session请求完成,就会开始渲染路由匹配到的子组件(如Counter.razor)。
子组件完成首轮渲染时,单例服务中靠后执行的异步请求还未返回、对应属性未赋值,而单例属性赋值时没有主动通知消费组件重绘,就会出现首轮渲染读到空值,必须手动触发StateHasChanged或页面跳转重渲染才能拿到值的现象。调换两个Session接口调用顺序后异常对象互换,本质就是时序问题导致的后返回值无法被子组件首轮渲染捕获。
另外Blazor WebAssembly运行在浏览器单线程环境,调用.Result阻塞异步任务会直接触发线程死锁,抛出*"Cannot wait on monitors on this runtime."*错误,因此所有HttpClient相关异步操作都必须使用await关键字,不能同步阻塞。
非级联参数的可行解决方案
方案1:根组件等待所有全局状态初始化完成后再渲染业务组件
在App.razor中增加初始化完成标记,所有需要全局加载的Session数据全部请求完成、赋值到单例服务后,再渲染路由对应的业务组件,从根源上避免子组件渲染时全局状态未就绪的问题。可以用Task.WhenAll并行发起多个Session请求,比顺序await加载速度更快。App.razor参考代码:
<Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> @if (_allStateLoaded) { <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> } else { <div class="loading">正在加载应用状态...</div> } </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">页面不存在</p> </LayoutView> </NotFound> </Router> @code { private bool _allStateLoaded; protected override async Task OnInitializedAsync() { // 并行发起所有Session请求,减少总加载时长 var authSessionTask = genericService.GetSession<IsAuthenticatedModel>("AuthModel"); var boozSessionTask = genericService.GetSession<Booz>("Booz"); // 等待所有请求全部返回 await Task.WhenAll(authSessionTask, boozSessionTask); // 统一赋值到单例服务 mzSingleton.Auth = await authSessionTask ?? new IsAuthenticatedModel(); mzSingleton.MyBooz = await boozSessionTask; // 标记加载完成,触发渲染业务组件 _allStateLoaded = true; } }
该方案无需修改任何业务组件逻辑,所有页面首次渲染时拿到的全局状态都是完整就绪的,是最直接的修复方式。
方案2:单例服务增加状态变更通知事件
如果需要保留App.razor异步加载过程中就渲染子组件的逻辑,可以在单例服务中实现状态变更事件,属性更新时主动触发事件,所有消费单例服务的组件订阅该事件,收到通知后自动触发自身重绘。
- 首先修改单例服务代码,增加变更通知逻辑:
public class MzSingleton { private IsAuthenticatedModel? _auth; private Booz? _myBooz; public IsAuthenticatedModel? Auth { get => _auth; set { _auth = value; TriggerStateChange(); } } public Booz? MyBooz { get => _myBooz; set { _myBooz = value; TriggerStateChange(); } } // 状态变更事件 public event Action? StateChanged; private void TriggerStateChange() => StateChanged?.Invoke(); }
- 在所有消费单例服务的组件(如Counter.razor)中订阅事件,组件销毁时取消订阅避免内存泄漏:
@implements IDisposable @inject MzSingleton mzSingleton {{-- 原有UI渲染逻辑保持不变 --}} @if (mzSingleton.Auth != null) { <h1>2 (serverside session)</h1> <label>Is Authenticated: </label><label>@mzSingleton.Auth.IsAuthenticated</label> <label>User: </label><label>@mzSingleton.Auth.User</label> @if (mzSingleton.Auth.Roles?.Count > 0) { foreach (string role in mzSingleton.Auth.Roles) { <label>Role: </label><label>@role</label> } } } @code { protected override void OnInitialized() { // 订阅单例状态变更事件 mzSingleton.StateChanged += OnSingletonStateChanged; } private void OnSingletonStateChanged() { // 收到变更通知后触发组件重绘 _ = InvokeAsync(StateHasChanged); } public void Dispose() { // 组件销毁时取消订阅,避免内存泄漏 mzSingleton.StateChanged -= OnSingletonStateChanged; } }
该方案适配全局状态动态更新的场景,即使登录后单例的Auth属性发生变更,所有订阅组件都会自动响应更新。
内容的提问来源于stack exchange,提问作者mz1378

