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

Blazor WebAssembly全局共享单实例对象问题:Scoped服务注入后属性值为空

问题原因分析

你遇到的问题核心在于Blazor WebAssembly中Scoped服务的作用域机制。当你在Program.cs里通过根服务提供者(serviceProvider.GetRequiredService<AccountService>())获取Scoped实例时,这个实例是属于根容器的独立对象;而页面组件中注入的Scoped实例,是Blazor为组件渲染上下文创建的全新实例——两者完全不是同一个对象,所以你在根容器里设置的_currentAccount自然不会出现在页面的实例中。

Blazor WASM的Scoped服务生命周期是和组件渲染树绑定的,每个客户端会话(浏览器标签页)会维护一个组件作用域,所有在这个作用域内的组件共享同一批Scoped服务实例,但根容器获取的实例不在这个作用域里。


解决方案

根据你的需求,这里提供两种可靠的解决思路:

方案一:将AccountService改为Singleton服务(全局唯一实例)

如果你的AccountService不需要依赖Scoped生命周期的服务(或者可以调整依赖获取方式),可以将其注册为Singleton,这样整个应用只会有一个实例,在Program.cs初始化的状态会被所有页面共享。

步骤:

  1. 修改AccountService,通过IServiceProvider动态获取Scoped依赖(因为HttpCaller和AuthenticationStateProvider是Scoped,不能直接注入到Singleton中):
public class AccountService { 
    private readonly IServiceProvider _serviceProvider;
    public AccountViewModel CurrentAccount { get; private set; } 

    public AccountService(IServiceProvider serviceProvider) { 
        _serviceProvider = serviceProvider;
    } 

    public async Task<OperationResult<AccountViewModel>> GetCurrentAccount() { 
        // 创建临时作用域获取Scoped服务
        using var scope = _serviceProvider.CreateScope();
        var httpCaller = scope.ServiceProvider.GetRequiredService<HttpCaller>();
        var authProvider = scope.ServiceProvider.GetRequiredService<AuthenticationStateProvider>();

        if (await UserIsAuthenticated(authProvider)) { 
            return await httpCaller.HttpCall<bool, AccountViewModel>(API.GetCurrentAccount, true, true); 
        } 
        return NotAuthenticated<AccountViewModel>(); 
    } 

    private async Task<bool> UserIsAuthenticated(AuthenticationStateProvider authProvider)
    {
        var authState = await authProvider.GetAuthenticationStateAsync();
        return authState.User.Identity?.IsAuthenticated ?? false;
    }
}
  1. 在Program.cs中注册为Singleton:
builder.Services.AddSingleton<AccountService>();
  1. 保持你原来的初始化代码不变,因为现在获取的是全局唯一实例:
AccountService accountService = serviceProvider.GetRequiredService<AccountService>(); 
var result = await accountService.GetCurrentAccount(); 
if (result.IsSucces) { 
    accountService.CurrentAccount = result.Result; 
};

这样所有页面注入的AccountService都是同一个实例,CurrentAccount会保留你初始化的值。


方案二:在根组件中初始化Scoped服务

如果你想保持AccountService的Scoped生命周期(比如需要和其他Scoped服务共享上下文),可以在应用的根组件(比如MainLayout.razor或App.razor)中完成初始化,这样组件作用域内的所有页面都会共享同一个Scoped实例。

步骤:

  1. 保持AccountService的原有代码和Scoped注册不变:
builder.Services.AddScoped<AccountService>();
  1. 在MainLayout.razor中注入AccountService并在初始化时加载数据:
@inject AccountService AccountService
@* 你的布局代码 *@

@code {
    protected override async Task OnInitializedAsync()
    {
        // 仅当未初始化时执行
        if (AccountService._currentAccount == null)
        {
            var result = await AccountService.GetCurrentAccount();
            if (result.IsSucces)
            {
                AccountService._currentAccount = result.Result;
            }
        }
    }
}

因为MainLayout是所有页面的父组件,它的Scoped服务实例会被所有子页面共享,所以初始化后,页面中注入的AccountService会带有已赋值的_currentAccount。


额外建议
  • 尽量避免使用公共字段(比如_currentAccount),改为属性并添加访问修饰符(比如public AccountViewModel CurrentAccount { get; private set; }),这样更符合封装原则,也能避免意外修改。
  • 如果GetCurrentAccount可能在应用运行中被多次调用,可以考虑添加缓存逻辑,避免重复请求。

内容的提问来源于stack exchange,提问作者Daniel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 19:19:09