Server-Side Blazor作用域状态容器实现:解决服务重复实例化问题
关于Server-Side Blazor作用域状态容器的问题解答
1. 你的假设是否正确?
不完全正确。Server-Side Blazor中,作用域服务的生命周期默认与用户的SignalR电路(会话)绑定,每个用户会话对应一个作用域实例,正常情况下不会无理由地实例化两次。如果出现你描述的情况,大概率是操作层面的问题,比如:
- 未通过依赖注入(DI)获取服务实例,而是直接手动构造了
StateContainer - 误在单例服务、全局中间件等跨作用域场景中注入了作用域服务
- 虽然你认为和预渲染无关,但仍需确认是否彻底禁用了预渲染(比如
_Host.cshtml中是否仍使用了RenderMode.ServerPrerendered)
2. 是预期行为还是疏漏?
如果是预渲染导致的两次实例化,属于预期行为——预渲染阶段会创建一个临时作用域生成静态HTML,实际建立SignalR连接后会创建新的作用域。但如果是完全禁用预渲染后仍出现两次实例化,那就是操作失误,而非框架疏漏。
3. 如何解决并实现状态容器?
首先修正基础问题
- 确保所有组件/服务都通过DI获取
IStateContainer,禁止手动实例化 - 彻底禁用预渲染(如果不需要):将
_Host.cshtml中的RenderMode.ServerPrerendered改为RenderMode.Server
正确实现状态容器及组件
完善状态容器的事件订阅清理
组件中订阅事件后,必须在销毁时取消订阅,避免内存泄漏和无效引用:
public class MyComponent : ComponentBase, IDisposable { [Inject] public IStateContainer StateContainer { get; set; } = null!; private bool _loadInitialData; protected override void OnInitialized() { StateContainer.SettingsChanged += StateContainer_SettingsChanged; _loadInitialData = StateContainer.Settings.LoadInitialData; } private void StateContainer_SettingsChanged(Settings settings) { _loadInitialData = settings.LoadInitialData; StateHasChanged(); } public void Dispose() { StateContainer.SettingsChanged -= StateContainer_SettingsChanged; } }
处理预渲染场景(如果需要保留预渲染)
如果必须使用预渲染,需要将状态在预渲染和实际会话间同步:
使用
ISession存储状态:- 先配置Session支持:在
Program.cs中添加builder.Services.AddSession();和app.UseSession(); - 在状态容器中通过
IHttpContextAccessor读写Session:public class StateContainer : IStateContainer { private readonly IHttpContextAccessor _httpContextAccessor; private Settings? _settings; public StateContainer(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public event Action<Settings>? SettingsChanged; public Settings Settings { get { if (_settings == null) { // 从Session读取 var sessionData = _httpContextAccessor.HttpContext?.Session.GetString("Settings"); _settings = sessionData != null ? JsonSerializer.Deserialize<Settings>(sessionData) : new Settings(); } return _settings; } set { _settings = value; // 写入Session _httpContextAccessor.HttpContext?.Session.SetString("Settings", JsonSerializer.Serialize(value)); SettingsChanged?.Invoke(value); } } } - 注意:
IHttpContextAccessor需要注册为单例:builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
- 先配置Session支持:在
或者使用电路级状态:通过
CircuitHandler跟踪电路,将状态与电路ID绑定,确保预渲染后的实际会话能获取到正确状态。
替代方案:使用Singleton服务配合用户标识
如果你需要跨会话的全局状态,或者不想处理作用域问题,也可以将StateContainer注册为单例,但需要在内部维护每个用户的独立状态(比如用字典存储,键为用户ID或电路ID):
public class StateContainer : IStateContainer { private readonly Dictionary<string, Settings> _userSettings = new(); private readonly IHttpContextAccessor _httpContextAccessor; public StateContainer(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public event Action<Settings>? SettingsChanged; private string CurrentUserId => _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? _httpContextAccessor.HttpContext?.Connection.Id ?? throw new InvalidOperationException("无法获取用户标识"); public Settings Settings { get { if (!_userSettings.TryGetValue(CurrentUserId, out var settings)) { settings = new Settings(); _userSettings[CurrentUserId] = settings; } return settings; } set { _userSettings[CurrentUserId] = value; SettingsChanged?.Invoke(value); } } }
这种方式需要确保用户已认证,或者用连接ID作为标识,注册时用builder.Services.AddSingleton<IStateContainer, StateContainer>();
内容的提问来源于stack exchange,提问作者Faygris
相关产品推荐
相关产品推荐

