Blazor Server中Scoped状态服务实例异常及数据为空问题咨询
Blazor Server 会话状态共享解决方案
一、先排查Scoped服务实例不一致的原因
Blazor Server中AddScoped注册的服务默认是电路级共享的,同一会话(同一个电路)下的所有组件应该拿到同一个实例。如果出现实例不同/为null,先排查这几点:
- 确认组件不是静态渲染模式(静态渲染的组件不会绑定到电路,每次请求都会创建新实例),如果用了静态渲染,需要改为交互式渲染(比如
@rendermode InteractiveServer)。 - 检查
Program.cs中服务注册的顺序,确保AddScoped<CurrentState>()在AddRazorComponents()之前或者正确的位置,没有被覆盖。 - 确认组件中注入的类名和注册的完全一致(比如大小写、命名空间),避免因类名不匹配导致注入失败为null。
二、正确实现Scoped状态服务的初始化(针对CustomStateService的问题)
你在OIDC的OnTokenValidated事件中初始化CustomStateService时,要注意:Blazor Server的Scoped服务是和电路关联的,而OIDC的验证事件是在HTTP请求管道中执行的,此时对应的电路可能还没创建,或者无法直接获取到Scoped服务实例。正确的做法是通过AuthenticationStateProvider来获取用户信息,或者在组件初始化时加载状态:
方案1:通过AuthenticationStateProvider获取用户信息
- 自定义
CustomStateService,依赖注入AuthenticationStateProvider:
public class CustomStateService { private readonly AuthenticationStateProvider _authProvider; private UserItem? _userItem; public CustomStateService(AuthenticationStateProvider authProvider) { _authProvider = authProvider; // 监听身份状态变化 _authProvider.AuthenticationStateChanged += OnAuthStateChanged; } public UserItem? UserItem => _userItem; private async void OnAuthStateChanged(Task<AuthenticationState> authStateTask) { var authState = await authStateTask; if (authState.User.Identity?.IsAuthenticated == true) { // 从Claims中提取用户信息,或者调用API获取 _userItem = new UserItem { Id = authState.User.FindFirstValue(ClaimTypes.NameIdentifier), Name = authState.User.FindFirstValue(ClaimTypes.Name) // 其他字段 }; } else { _userItem = null; } } }
- 在
Program.cs中注册为Scoped:
builder.Services.AddScoped<CustomStateService>();
- 组件中注入后,可通过
UserItem获取数据,也可以监听状态变化(比如用StateHasChanged()更新UI)。
方案2:在组件初始化时手动初始化状态
如果必须在OIDC验证后初始化,可将用户信息存储到HttpContext.Session,然后在组件的OnInitializedAsync中从Session读取并填充到CustomStateService:
- 先启用Session:
builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); }); // 在管道中添加Session中间件 app.UseSession();
- OIDC的
OnTokenValidated事件中存储用户信息到Session:
.AddOpenIdConnect(options => { // 其他配置 options.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { var userItem = new UserItem { // 从context.Principal提取信息 }; // 序列化UserItem到Session context.HttpContext.Session.SetString("UserItem", JsonSerializer.Serialize(userItem)); } }; });
- 在
CustomStateService中注入IHttpContextAccessor(需要先注册builder.Services.AddHttpContextAccessor();),然后在组件初始化时读取:
public class CustomStateService { private readonly IHttpContextAccessor _httpContextAccessor; public UserItem? UserItem { get; set; } public CustomStateService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public void LoadUserFromSession() { var session = _httpContextAccessor.HttpContext?.Session; if (session != null) { var userJson = session.GetString("UserItem"); if (!string.IsNullOrEmpty(userJson)) { UserItem = JsonSerializer.Deserialize<UserItem>(userJson); } } } }
- 组件中调用:
@inject CustomStateService customStateService @code { protected override async Task OnInitializedAsync() { customStateService.LoadUserFromSession(); await base.OnInitializedAsync(); } }
三、级联参数方案(适合简单状态共享)
如果不想用依赖注入,可在Routes.razor中管理全局状态,通过级联参数传递给所有子组件:
- 修改
Routes.razor,添加状态类并级联:
@inject AuthenticationStateProvider AuthProvider <CascadingValue Name="CurrentUserState" Value="@_currentState" IsFixed="false"> <Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> <PageTitle>Not found</PageTitle> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingValue> @code { private CurrentState _currentState = new(); protected override async Task OnInitializedAsync() { var authState = await AuthProvider.GetAuthenticationStateAsync(); if (authState.User.Identity?.IsAuthenticated == true) { // 初始化_currentState的参数 _currentState.UserItem = new UserItem { // 填充数据 }; } } }
- 子组件中接收级联参数:
@code { [CascadingParameter(Name = "CurrentUserState")] public CurrentState? CurrentState { get; set; } protected override void OnParametersSet() { // 使用CurrentState中的数据 if (CurrentState != null) { // 逻辑处理 } } }
这种方式的好处是所有子组件共享同一个CurrentState实例,状态更新时只要修改_currentState并调用StateHasChanged(),所有接收级联参数的组件都会更新。
四、总结
- 不需要强制使用Session,Blazor Server的Scoped服务本身就支持同电路(会话)内的实例共享,先排查实例不一致的原因(渲染模式、注册顺序、类名匹配)。
- 状态初始化要结合Blazor的生命周期,避免在HTTP管道事件中直接操作Scoped服务,优先通过
AuthenticationStateProvider监听身份状态变化。 - 级联参数适合简单的全局状态共享,实现成本低,无需依赖注入配置。
内容的提问来源于stack exchange,提问作者Tobias Gustafsson
相关产品推荐
相关产品推荐

