为何Blazor的Scoped服务持续生成新实例?依赖注入问题咨询
配置与代码示例
Program.cs 服务注册
builder.Services.AddScoped<SessionData>();
SessionData.cs 类实现
public class SessionData { public bool ShowPhoneConfirmationBanner { get; set; } public int Id { get; } = _id++; private static int _id = 0; }
MainLayout.razor 注入与使用
[Inject] private SessionData SessionData { get; set; } = default!; protected override async Task OnInitializedAsync() { bool show = SessionData.ShowPhoneConfirmationBanner; // 业务逻辑处理... }
问题现象
- 首次加载Index.razor时,
OnInitializedAsync()方法被调用两次,每次获取的SessionData实例都是不同的对象 - 切换站内任意页面时,
MainLayout.OnInitializedAsync()会触发,且每次都会得到新的SessionData实例
测试的导航方式
- 点击页面原生
<a href=...>链接跳转站内页面 - 在浏览器地址栏直接输入站内页面的完整URL
- 调用
NavigationManager.NavigateTo(url);方法进行跳转 - 使用DxMenuItem.NavigateUrl(最终渲染为原生
<a>标签)
原因分析
Blazor中Scoped服务的生命周期绑定到电路(Circuit)和组件树,出现每次导航都生成新实例的情况,核心原因如下:
整页刷新触发电路重建:使用原生
<a>标签跳转、地址栏输入URL,都会触发浏览器整页刷新,这会完全重建Blazor电路。由于Scoped服务是电路级别的,电路重建后自然会创建新的服务实例。如果调用NavigationManager.NavigateTo时设置了forceLoad: true,同样会触发整页刷新。预渲染导致的双实例:Blazor Server默认启用服务器端预渲染(Prerendering),首次加载时会先在服务器生成静态HTML,此时会创建一个临时的Scoped实例;当客户端与服务器建立实时连接后,会重新创建一个用于交互式渲染的实例,这就是首次加载出现两次不同实例的原因。
组件树随电路重建:整页刷新会导致整个组件树销毁重建,即使Scoped服务是电路级,电路重建后组件重新注入时也会获取新实例。
解决方案
1. 改用客户端导航避免电路重建
- 替换原生
<a>标签为Blazor内置的NavLink组件,它会触发客户端导航,不会导致整页刷新:<NavLink href="/target-page">跳转至目标页面</NavLink> - 使用
NavigationManager.NavigateTo时,保持默认的forceLoad: false(客户端导航模式):NavigationManager.NavigateTo("/target-page"); - 对于DxMenuItem,若默认的
NavigateUrl触发整页刷新,可手动绑定点击事件,调用NavigationManager.NavigateTo实现客户端导航。
2. 消除预渲染带来的实例差异
如果不需要预渲染,可直接禁用它:在_Host.cshtml中修改组件渲染模式,将render-mode="ServerPrerendered"改为render-mode="Server":
<component type="typeof(App)" render-mode="Server" />
若需要保留预渲染,可通过ISession或ProtectedSessionStorage在预渲染和交互式渲染之间同步数据。
3. 谨慎使用Singleton服务
如果需要全局唯一的实例(所有用户共享),可将服务注册为Singleton,但注意这种方式不适合用户会话级数据,且要处理线程安全问题:
builder.Services.AddSingleton<SessionData>();
4. 使用Session存储会话级数据
对于用户专属的会话状态,更可靠的方式是使用ProtectedSessionStorage存储数据,即使电路重建也能从Session中恢复:
// 注入ProtectedSessionStorage [Inject] private ProtectedSessionStorage SessionStorage { get; set; } = default!; // 保存数据 await SessionStorage.SetAsync("ShowPhoneConfirmationBanner", true); // 获取数据 var result = await SessionStorage.GetAsync<bool>("ShowPhoneConfirmationBanner"); bool show = result.Success ? result.Value : false;
内容的提问来源于stack exchange,提问作者David Thielen

