Blazor Server:Singleton服务如何获取Scoped服务ProtectedSessionStorage的数据?
Blazor Server 中 Singleton 服务访问 Scoped 服务的解决方案
问题背景
我们在Blazor Server应用中碰到以下场景:
- 用户登录认证通过后展示公司列表
- 用户选中公司后,将
CompanyDBName和CompanyUi存入ProtectedSessionStorage(Scoped生命周期,从登录到注销有效) - 有一个必须为Singleton的
MainService,其DbConnectionString存储不含Initial Catalog的通用连接字符串,需要通过GetCompanyConnectionString方法添加CompanyDbName生成对应公司的连接字符串,供数据库工厂使用 - 直接在
MainService构造函数注入ProtectedSessionStorage会报错,因为Singleton服务不能依赖Scoped服务,生命周期不兼容
原错误代码:
public class MainService { private readonly ProtectedSessionStorage _protectedSessionStorage; public string DbConnectionString{ get; set; } = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=;Integrated Security=True"; // 构造函数注入Scoped服务会报错 public MainService(ProtectedSessionStorage protectedSessionStorage) { _protectedSessionStorage = protectedSessionStorage; } async Task<string> GetCompanyConnectionString() { var companyDbName = await _protectedSessionStorage.GetAsync<string>("UserDb"); SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DbConnectionString); builder.InitialCatalog = companyDbName; return builder.ToString(); } }
服务注册:
builder.Services.AddSingleton<MainService>();
解决方案
方案1:将CompanyDbName作为参数传入方法
这是最直接且符合依赖原则的方案——让调用方(通常是Scoped组件/服务)负责获取CompanyDbName,再传给MainService的方法。这样MainService无需依赖Scoped服务,保持Singleton的纯洁性。
修改后的MainService:
public class MainService { public string DbConnectionString{ get; set; } = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=;Integrated Security=True"; public MainService() { // 无需注入Scoped服务 } // 直接接收CompanyDbName参数 string GetCompanyConnectionString(string companyDbName) { if(string.IsNullOrEmpty(companyDbName)) throw new ArgumentNullException(nameof(companyDbName)); SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DbConnectionString); builder.InitialCatalog = companyDbName; return builder.ToString(); } }
调用示例(在Blazor组件中):
@inject ProtectedSessionStorage ProtectedSessionStorage @inject MainService MainService @code { private async Task UseCompanyConnection() { var companyDbNameResult = await ProtectedSessionStorage.GetAsync<string>("UserDb"); if(companyDbNameResult.Success) { var connectionString = MainService.GetCompanyConnectionString(companyDbNameResult.Value); // 传给数据库工厂使用 } } }
方案2:通过IServiceProvider按需创建Scoped作用域
如果必须在MainService内部获取CompanyDbName,可以注入IServiceProvider,在方法内创建Scoped作用域来获取ProtectedSessionStorage。注意必须在Blazor的组件上下文中调用此方法(确保当前有活跃的Scoped会话)。
修改后的MainService:
public class MainService { private readonly IServiceProvider _serviceProvider; public string DbConnectionString{ get; set; } = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=;Integrated Security=True"; // 注入IServiceProvider(Singleton生命周期,安全) public MainService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } async Task<string> GetCompanyConnectionString() { // 创建Scoped作用域 using var scope = _serviceProvider.CreateScope(); var protectedSessionStorage = scope.ServiceProvider.GetRequiredService<ProtectedSessionStorage>(); var companyDbNameResult = await protectedSessionStorage.GetAsync<string>("UserDb"); if(!companyDbNameResult.Success || string.IsNullOrEmpty(companyDbNameResult.Value)) throw new InvalidOperationException("未找到选中的公司数据库名称"); SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DbConnectionString); builder.InitialCatalog = companyDbNameResult.Value; return builder.ToString(); } }
服务注册保持不变:
builder.Services.AddSingleton<MainService>();
注意事项
- 方案1优先推荐:符合单一职责,避免Singleton依赖Scoped服务的生命周期问题,代码更清晰
- 方案2需谨慎使用:必须确保调用方法时处于活跃的Blazor会话上下文(比如在组件事件、页面初始化中调用),如果在后台任务中调用,可能无法获取到正确的Session数据
- 避免使用
IHttpContextAccessor:Blazor Server中,HttpContext仅在初始页面加载时存在,后续SignalR交互中没有HttpContext,依赖它会导致不可靠的行为
内容的提问来源于stack exchange,提问作者Pedro Horta
相关产品推荐
相关产品推荐

