.NET 6 Blazor Server已判空属性getter仍抛空引用异常
问题根本原因
这个空引用异常是Blazor Server生命周期特性+异步方法执行逻辑共同导致的,和非空判断的写法无关:
- Blazor组件的生命周期方法
OnInitializedAsync是异步执行的,当方法执行到第一个await操作时,会立刻将控制权返回给Blazor渲染引擎,引擎会在此时触发首次组件渲染,而你await之后的代码(包括给RandomDish赋值的逻辑)还没执行,RandomDish此时保持默认的null值。 - 你写的
@if (RandomDish != null)判断在这次提前渲染时确实会返回false,但.NET 6版本的Razor编译器存在可空类型校验缺陷:哪怕你把@bind="RandomDish.Name"写在if块内部,编译器生成的渲染代码依然会直接调用RandomDish.get访问属性,不会继承if判断的非空上下文,直接触发空引用。 - 你手动给
RandomDish初始化new Dish()实例只是给了属性一个非null默认值,绕开了首次渲染的空访问,没有解决异步执行空窗期的状态管理问题,后续点击按钮触发异步查询时依然有概率出现同类异常。 - 额外注意:Blazor Server中
IHttpContextAccessor仅在组件首次初始化的极短时间内可用,在后续组件交互、重渲染流程中HttpContext会为null,你当前在OnInitializedAsync里直接取用户身份的写法本身就有概率触发额外的空异常。
正确修复方案
不要依赖业务实体的null值做渲染状态判断,通过独立的加载状态标记隔离异步执行空窗期:
- 新增独立的
bool isLoading状态字段,所有异步操作(初始化、重新查询、保存)执行前将其设为true,操作完成、所有实体字段赋值结束后再设为false。 - 渲染逻辑第一层优先判断
isLoading,加载状态下直接展示加载占位内容,完全不执行任何访问RandomDish、Region的逻辑,从根源上避免空窗期访问空属性。 - 修正DbContext的生命周期:通过
IDbContextFactory创建的DbContext不会被DI容器自动释放,需要组件实现IAsyncDisposable接口,在组件销毁时主动释放DbContext实例,避免连接泄漏。 - 用户身份信息在
OnInitializedAsync首次执行时一次性取出存到本地字段,不要在后续的交互、渲染逻辑中反复访问IHttpContextAccessor.HttpContext。 - 非空判断块内访问
RandomDish的成员时,可以使用!null包容运算符绕过编译器的可空校验,前提是你已经通过加载状态保证了代码执行到此处时RandomDish一定不为null。
参考实现代码:
@implements IAsyncDisposable @inject IDbContextFactory<MyDbContext> DbContextFactory @inject IHttpContextAccessor HttpContextAccessor @if (isLoading) { <div class="loading">数据加载中...</div> } else { <div>菜品名称:@RandomDish!.Name</div> <div>菜品ID:@RandomDish.DishId</div> <div>区域信息:@Region?.BoundaryText</div> <button @onclick="FetchNewRandomDish">GET NEW DISH</button> <div> <label>修改菜品名:</label> <input @bind="RandomDish!.Name" /> <button @onclick="SaveDishChanges">保存修改</button> </div> } @code { private MyDbContext? _dbContext; private Dish? RandomDish; private Region? Region; private bool isLoading; private ClaimsPrincipal? _currentUser; protected override async Task OnInitializedAsync() { isLoading = true; // 一次性取出用户信息,后续不再访问HttpContext _currentUser = HttpContextAccessor.HttpContext?.User; // 创建DbContext实例 _dbContext = DbContextFactory.CreateDbContext(); // 查询区域数据 Region = await _dbContext.Regions .FirstOrDefaultAsync(r => r.Name == "Italy"); // 初始化随机菜品 await FetchNewRandomDish(skipLoadingState: true); isLoading = false; } private async Task FetchNewRandomDish(bool skipLoadingState = false) { if (!skipLoadingState) isLoading = true; RandomDish = await _dbContext!.Dishes .OrderBy(d => Guid.NewGuid()) .FirstOrDefaultAsync(); if (!skipLoadingState) isLoading = false; // 赋值完成后主动触发一次渲染,刷新UI StateHasChanged(); } private async Task SaveDishChanges() { if (RandomDish == null) return; await _dbContext!.SaveChangesAsync(); } public async ValueTask DisposeAsync() { if (_dbContext != null) { await _dbContext.DisposeAsync(); } } }
可选优化:如果你不需要预渲染带来的首屏加载优化,可以把Pages/_Host.cshtml中组件标签的render-mode从ServerPrerendered改为Server,可以跳过初始化前的预渲染流程,减少一次无意义的渲染,但这不能替代加载状态的逻辑——后续按钮触发异步操作时依然会存在状态空窗。
内容的提问来源于stack exchange,提问作者Robert
相关产品推荐
相关产品推荐

