ASP.NET控制器调用发送邮件方法时出现空引用异常
空引用异常排查与解决
核心触发原因
结合代码和报错堆栈,这个异常的触发本质有三类常见场景:
- 异步方法的错误调用方式
SendEmail是返回Task的异步方法,但在同步签名的TransferAction里直接无等待调用(fire-and-forget模式),没有对返回的Task做任何异常捕获和上下文同步。当SendEmailAsync内部的await逻辑完成后,尝试回到原请求的SynchronizationContext时,原请求可能已经处理完成、上下文被释放,就会在SynchronizationContextAwaitTaskContinuation流程中触发空引用,且这类异常会被丢到线程池执行,不会被MVC正常的异常处理管线捕获,最终呈现你看到的堆栈。 - 前置数据查询缺失空值校验
你连续使用多次无保护的.First()查询,只要任意一步查询没有匹配到结果,就会直接拿到null值:Nurse_Account表中没有匹配当前nurse_account_id的记录,取出的nurse_account_user_id为空AspNetUsers表中没有匹配用户ID的记录,取出的usr或usr_mail为空
这些空值传入SendEmailAsync内部,同样会触发空引用。
- 邮件服务组件未正确配置
如果ApplicationUserManager在初始化时没有给EmailService属性赋值实际的邮件发送实现,SendEmailAsync内部执行到获取邮件服务逻辑时就会拿到null,直接触发异常。
对应解决步骤
- 修正异步调用逻辑
不要使用无等待的fire-and-forget调用,将TransferAction改为异步签名,全程使用await传递异步调用,避免上下文丢失:
// 将同步Action改为异步签名 public async Task<ActionResult> Transfer(int id, int nurse_id, int ambulance_id, int driver_id, int nurse_account_id) { // ...原有业务逻辑 // 替换原有查询逻辑,增加空值校验 var nurseUserId = db.Nurse_Account.Where(m => m.nurse_account_id == call.nurse_account_id) .Select(m => m.nurse_account_user_id) .FirstOrDefault(); if (string.IsNullOrEmpty(nurseUserId)) { return Json(new { result = "发送失败:未匹配到关联护士账号" }, JsonRequestBehavior.AllowGet); } var targetUser = db.AspNetUsers.Where(m => m.Id == nurseUserId) .Select(m => new { m.Id, m.Email }) .FirstOrDefault(); if (targetUser == null || string.IsNullOrEmpty(targetUser.Email)) { return Json(new { result = "发送失败:未匹配到护士对应的账号邮箱" }, JsonRequestBehavior.AllowGet); } // 直接await异步发送方法,不要无等待调用 await SendEmail(targetUser.Id, "New call", targetUser.Email); // ...后续原有业务逻辑 return Json(new { result = "OK" }, JsonRequestBehavior.AllowGet); }
注意不要在异步方法中使用.Wait()或.Result同步阻塞等待,这类写法很容易造成请求死锁。
校验UserManager的邮件服务配置
找到项目中ApplicationUserManager的初始化逻辑(一般在App_Start/IdentityConfig.cs中),确认已经给EmailService属性赋值了实际的SMTP邮件发送实现,否则SendEmailAsync会因为找不到邮件服务实例触发空引用。临时定位技巧
如果暂时无法快速修改异步逻辑,可以临时用同步阻塞的方式包裹SendEmail调用,捕获真实的内部异常来定位问题:
try { // 仅用于临时排查,正式环境不要使用这种写法 SendEmail(usr, "New call", usr_mail).GetAwaiter().GetResult(); } catch (Exception ex) { // 此时可以拿到内部真实的异常堆栈,定位具体的空对象来源 var realError = ex.GetBaseException(); // 记录错误信息后继续处理 }
内容的提问来源于stack exchange,提问作者zinon
相关产品推荐
相关产品推荐

