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

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;
    }
}

处理预渲染场景(如果需要保留预渲染)

如果必须使用预渲染,需要将状态在预渲染和实际会话间同步:

  1. 使用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>();
  2. 或者使用电路级状态:通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 15:45:13