Blazor跨Razor页面传选中行ID供C# SQL查询的多用户实现
跨Razor页传递用户选中行ID的最佳实践
静态变量方案为什么必然出问题
- 别再靠静态变量存选中的行ID了,这个方案从设计原理上就不成立:静态变量是整个应用进程级的全局共享资源,所有访问应用的用户会读写同一个变量实例。只要有一个用户更新了值,其他所有用户读到的都是被篡改过的最新值,多用户操作互相干扰是必然结果,不是调试能修好的bug。
首选方案:导航时直接带ID(零额外状态存储)
90%以上的"列表选行→进目标页筛选数据"的场景,根本不需要存任何临时状态,直接在页面导航时把ID带过去就行,完全没有跨用户污染的风险,还天然支持页面刷新、链接分享这些友好特性:
- 路由参数传递:如果ID不需要隐藏,直接把ID设计成路由的一部分
- 列表页选中行后,用
NavigationManager导航到目标页时把ID拼到路由里:// selectedRow.Id 是当前用户选中的行主键 NavigationManager.NavigateTo($"order-detail/{selectedRow.Id}"); - 目标页定义对应路由,Blazor会自动从路由里提取参数赋值给对应属性:
@page "/order-detail/{selectedId:int}" [Parameter] public int SelectedId { get; set; }
- 列表页选中行后,用
- 查询字符串传递:如果不想改原有路由结构,或者需要传多个筛选条件,就把ID附在导航URL的查询参数里,目标页加个
SupplyParameterFromQuery特性就能自动接收值,实现逻辑和路由参数一致。
不能靠URL传ID时的隔离状态方案
如果业务逻辑要求ID不能暴露在URL上(比如敏感数据、导航过程还有多步校验),就根据你用的Blazor托管模式选完全隔离的状态载体,根本没必要优先用传统会话变量:
- Blazor Server 模式:用Scoped 服务存临时状态
Blazor Server下每个用户的SignalR连接是独立的电路(Circuit),注册为Scoped的服务会完全和当前用户的电路绑定,不同用户的服务实例在内存里是完全隔离的,绝对不会串值。
实现非常简单:- 写一个简单的状态类,用来存当前选中的行ID:
public class SelectedRowState { public int? SelectedId { get; set; } } - 在Program.cs里把这个类注册为Scoped服务:
builder.Services.AddScoped<SelectedRowState>(); - 列表页和目标页都注入这个服务,列表页选中行时给
SelectedId赋值,目标页读到值后直接用来做SQL查询就行。用户关了浏览器断开连接,对应的Scoped状态会自动被GC回收,没有残留数据。
- 写一个简单的状态类,用来存当前选中的行ID:
- Blazor WebAssembly / MAUI Blazor 客户端模式:本身就没有服务器端会话的概念,同样用Scoped服务存临时状态就行——客户端模式下Scoped的生命周期和当前浏览器Tab/应用实例绑定,多用户之间的内存完全隔离,不存在串值问题。如果需要刷新页面后还能保留这个ID,就把值写到浏览器/应用的本地存储里,同样是每个用户独立的存储配额,不会互相干扰。
会话变量什么时候才需要用
会话变量不是洪水猛兽,但绝对不要把它当Blazor内部页面跳转的首选方案:
- 只有当你的Blazor应用需要和传统ASP.NET Core MVC/Razor Pages页面混合跳转,传统页面没法直接读取Blazor的Scoped状态时,才需要把选中ID写到会话变量里供跨技术栈的页面读取。
- 会话变量本身有一堆坑:默认是存在服务器内存里的,多实例部署必须额外配置分布式缓存;有固定超时时间;每次读Session有额外的序列化开销。纯Blazor内部跳转的体验和可靠性,远不如前面说的导航参数、Scoped服务方案。
几个容易踩的坑
- 千万别用Singleton服务存这类用户选中的行状态,Singleton是全局单例,和静态变量的缺陷完全一致——所有用户共享同一个实例,必然多用户串值。
- 不管用哪种方式拿到ID,都要先做权限校验:确认当前用户确实有权限访问这个ID对应的数据,再把它作为SQL查询参数,避免越权访问。
- 不要把整行的实体数据存在临时状态里,拿到ID后再从数据库重新查询对应数据,避免出现数据过期、EF Core跟踪异常这类问题。
内容的提问来源于stack exchange,提问作者Geoff
相关产品推荐
相关产品推荐

