You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET6 Blazor WebAssembly OnInitializedAsync初始化单例服务子页面取值异常

Blazor WebAssembly 刷新后单例服务值读取异常问题

问题根因

.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异步加载过程中就渲染子组件的逻辑,可以在单例服务中实现状态变更事件,属性更新时主动触发事件,所有消费单例服务的组件订阅该事件,收到通知后自动触发自身重绘。

  1. 首先修改单例服务代码,增加变更通知逻辑:
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();
}
  1. 在所有消费单例服务的组件(如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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 04:57:08