ASP.NET Core中调用存储proc后外键关联对象为null的处理方法
问题根源
当你用EF Core的FromSqlRaw调用存储过程时,EF Core不会自动加载导航属性(比如你的RequestingUser)。存储过程仅返回PendingPOC实体的字段(包括RequestingUserId),但EF Core不会主动查询关联的UserViewModel数据来填充这个导航属性,因此它始终为null,最终触发空引用异常。
解决方法
方法1:手动批量查询并填充导航属性
这是最直接可靠的方案,适合新手快速解决问题:
- 先获取所有
PendingPOC记录 - 收集所有不重复的
RequestingUserId - 批量查询这些ID对应的用户数据
- 把用户数据逐个赋值给
PendingPOC的RequestingUser属性
修改服务层GetSettingsAsync方法:
public async Task<IEnumerable<PendingPOC>> GetSettingsAsync(string id) { var user = _httpContextAccessor.HttpContext?.User; UserId = _userManager.GetUserId(user); LabelerCode = "XXXX01"; //hardcoded for now List<SqlParameter> paramList = new() { new SqlParameter { ParameterName = "@UserId", Value = UserId}, new SqlParameter { ParameterName = "@LCode", Value = LabelerCode} }; // 1. 获取PendingPOC列表 var pendingPocs = await _context.PendingPocDbSet .FromSqlRaw(@"EXEC GetRequests_SP @UserId, @LCode", paramList.ToArray()) .AsNoTracking() // 无需追踪,手动赋值即可 .ToListAsync(); if (!pendingPocs.Any()) return pendingPocs; // 2. 收集所有需要查询的用户ID var userIds = pendingPocs.Select(p => p.RequestingUserId).Distinct().ToList(); // 3. 批量查询用户数据(根据你的实体结构调整映射逻辑) var users = await _userManager.Users .Where(u => userIds.Contains(u.Id)) .ToListAsync(); // 4. 手动填充导航属性 foreach (var poc in pendingPocs) { var matchingUser = users.FirstOrDefault(u => u.Id == poc.RequestingUserId); if (matchingUser != null) { poc.RequestingUser = new UserViewModel { FirstName = matchingUser.FirstName, // 按需添加其他需要的用户属性 }; } } return pendingPocs; }
方法2:修改存储过程返回关联用户数据(可选)
如果你有权限修改存储过程,可以让它直接返回PendingPOC和用户的关联字段,再手动映射到导航属性:
- 修改存储过程
GetRequests_SP,JOIN用户表返回所需字段 - 在
PendingPOC模型中添加临时属性存储用户字段 - 获取结果后用临时属性填充
RequestingUser对象
示例存储过程修改(伪SQL):
CREATE PROCEDURE GetRequests_SP @UserId NVARCHAR(450), @LCode NVARCHAR(10) AS BEGIN SELECT p.*, u.FirstName AS RequestingUserFirstName -- 按需添加其他用户字段 FROM PendingPOC p JOIN AspNetUsers u ON p.RequestingUserId = u.Id -- 保留原存储过程的WHERE等逻辑 END
修改PendingPOC模型:
// 新增临时属性,用于接收存储过程返回的用户字段 public string RequestingUserFirstName { get; set; } // 原外键与导航属性 [Required] public string RequestingUserId { get; set; } [ForeignKey("RequestingUserId")] public virtual UserViewModel? RequestingUser { get; set; }
服务层处理逻辑:
var pendingPocs = await _context.PendingPocDbSet .FromSqlRaw(@"EXEC GetRequests_SP @UserId, @LCode", paramList.ToArray()) .ToListAsync(); // 填充导航属性 foreach (var poc in pendingPocs) { poc.RequestingUser = new UserViewModel { FirstName = poc.RequestingUserFirstName // 按需添加其他属性 }; }
方法3:尝试EF Core的Include(限制较多)
注意:EF Core对存储过程使用Include有严格要求,仅当存储过程返回的结果集完全匹配实体及其导航属性的列名时才可能生效,且需要开启追踪:
var result = await _context.PendingPocDbSet .FromSqlRaw(@"EXEC GetRequests_SP @UserId, @LCode", paramList.ToArray()) .Include(p => p.RequestingUser) // 尝试加载导航属性 .AsTracking() // 必须开启追踪才能触发关联查询 .ToListAsync();
此方法成功率较低,因为存储过程的结果集通常无法自动触发EF Core的关联查询,更推荐方法1。
额外防御性建议
在Razor页面中添加空值判断,避免即使修复后仍出现异常:
<td> @(pendingPocItem.RequestingUser?.FirstName ?? "Unknown User") </td>
内容的提问来源于stack exchange,提问作者L. Wallach
相关产品推荐
相关产品推荐

