Blazor Server中Scoped注入DB Context的数据同步问题求助
问题描述
我们基于Blazor Server开发应用,使用DevExpress网格组件直接从DB Context取数,希望过滤、分组等操作在数据库层执行,因此没有通过服务层获取数据。
假设有两名用户在各自浏览器(对应不同SignalR连接)查看同一网格,用户1修改数据状态后,用户2刷新网格无法看到变化,只有页面刷新(F5)时才会显示差异。
原因分析
DB Context默认采用Scoped DI模式。在传统HTTP请求-响应架构中,DI会为每个请求提供同一个DB Context实例,每次请求都会刷新数据。
但在Blazor应用中,DB Context不会随每个Web请求刷新,SignalR(WebSocket)中甚至没有“请求”概念。只要SignalR连接活跃,用户2就会使用同一个DB Context实例,其他用户的状态修改不会同步到该实例,此时Scoped的DB Context几乎相当于用户会话/SignalR连接范围内的Singleton。
思考
- 服务层无状态无问题,DB Context是核心痛点
- Blazor没有传统“Scoped”服务概念
- Scoped实际是单个连接范围内的Singleton
- Singleton为所有连接提供同一服务实例
- 存在组件级近似Scoped服务实现
- 每个Razor组件生命周期内使用同一实例
- 但组件生命周期可能较长
- 与传统架构类似:同时请求会有不同状态的DB Context实例,但概率低,问题不突出
- Transient模式的DB Context不可行
- 期望API(服务层)方法作为“工作单元”(1个API对应1个数据库事务)
- 单个API可能调用多个ServiceBL类的业务函数,需共用同一DB Context实例
现有方案
- 将DB Context注册为Singleton?
- 风险高:所有用户共用长实例,易成性能瓶颈,且存在线程安全问题
- EF Core不支持同一Context实例并行执行多操作
- 合适时机触发页面刷新替代Scope机制
await JSRuntime.InvokeVoidAsync("location.reload");NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
- 点击网格刷新按钮时创建新DB Context实例
- 仅解决当前场景,底层多用户DB Context不同步问题仍存在
- 以API方法为工作单元,手动创建DI Scope并销毁,但需手动传递DB Context到所有需用类,实现繁琐
优化思路
1. 组件级生命周期绑定DB Context Scope
基于Razor组件的生命周期创建独立的DI Scope:组件初始化时创建Scope并获取DB Context,组件销毁时释放Scope。这样每个组件实例拥有专属Context,网格刷新、过滤等操作都会从新Context拉取最新数据,同时保证同一组件内的业务操作共用同一Context,满足工作单元的事务需求。
2. 查询操作临时创建Context
针对网格的查询类操作(刷新、过滤、分组),每次触发时通过IDbContextFactory临时创建DB Context实例,完成查询后立即释放。这种方式确保每次查询都获取数据库最新状态,而写操作仍通过手动Scope或组件级Scope保证事务一致性。
3. 全局数据变更通知机制
实现轻量级事件总线,当用户完成数据修改时发布对应的数据变更事件。其他用户的网格组件订阅该事件,收到通知后自动触发数据刷新(使用新Context重新查询),无需用户手动刷新页面即可同步状态。
4. 模拟请求级Scope的工作单元
利用IDbContextFactory,在每个业务方法(无论是查询还是写操作)开始时创建Context,操作完成后释放。对于跨多个服务的事务场景,手动创建一个临时DI Scope,在Scope内统一获取Context并传递给相关服务,操作结束后销毁Scope,既保证工作单元的事务性,又避免长时间持有Context导致的缓存不一致。
5. 关闭查询结果跟踪
将DB Context的查询跟踪模式设置为NoTracking,这样查询返回的实体不会被Context缓存,每次查询都会直接从数据库获取最新数据。该方式适用于只读场景,写操作仍需注意Context的生命周期管理。
内容的提问来源于stack exchange,提问作者berserker

