前天当我为新项目新增完日志模块后对日志模块进行测试,测试时居然发现开发人员一段非常简单的代码,而且很标准的try ... catch .. 写法。代码整理如下:
public JsonResult SaveTest()
{
try
{
//LinqToSql:返回IQueryable数据集合。
var iQueryableData = (from o in _Context.Orders//.Where(o => o.OrderID == 10248)
select new
{
ShipName = o.ShipName,
Employee = o.Employee,
}).ToList();
//LINQ:返回IEnumerable集合。
var iEnumerableData = from d in iQueryableData
select new
{
ShipName = d.ShipName,
EmployeeName = d.Employee.LastName //空引用未处理引发程序异常。
};
return Json(new { Success = true, Msg = iEnumerableData }, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(new { Success = false, Msg = ex.Message }, JsonRequestBehavior.AllowGet);
}
}
为方便大家阅读,我用 NORTHWIND 数据库。同时在该数据库内执行 SQL :update orders set EmployeeID =null where OrderID =10248 。这样造成上述代码第18 行Linq代码迭代时产生异常。您认为 这个异常可以被catch住么?答案当然是否定的!
当发现这个Bug后顿时让我产生了兴趣,我不知道如果微软的MVC项目开发人员看到这个bug,他是如何解释的。接下来让我就来一个非官方解释吧,欢迎拍砖!
我们可以在第18行设置好断点,然后用VS2010 Debug程序,发现执行到第 21 行 return Json 时 VS未报任何bug。过数秒后断点直接定位在第18行,提示也很简单:未将对象应用到实例。这个确实是我们预期想要的 Exception 但是它始终没有被 catch 住!通过StackTrace我发现了这么一句提示: at System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) 。上述代码第21行return Json 被执行后返回了 JsonAction 这个 JsonAction 被 InvokeActionResult 调用?难道这个就是问题的关键所在?原因是写有try catch 函数(本例的SaveTest()函数)的外层调用函数(InvokeActionResult()函数)出现了异常。也就是外层函数出现异常我们的内层函数SaveTest()又怎能try catch 到这个外层异常呢?于是通过Reflector进一步证实我的想法。具体的请看下图:
主要是这句action.ExecuteResult(ControllerContext);它的执行过程如下:
最后一幅图的JavaScriptSerializer serializer = new JavaScriptSerializer(); 进行 json 序列化时这个外层函数报出了异常。这个异常在我们的内层函数 SaveTest() 方法内是不可能被截获到的。此刻我已完全弄明白这个无法被catch 的exception 是怎么出现的!至于这个异常为什么会被外层函数触发,主要是.net Linq 延迟加载机制导致的,这不属于本文讨论的范畴,具体的园子内很多文章已经解释的很清楚了。
为消灭掉这个截获不到的exception现附上解决方案:
MVC虽然很年轻,但是这个架构真的很优秀我们可以非常方便对其扩展(我不是MVC的托,呵呵)。为了解决问题,我们只要自定义一个特性该特性继承 HandleErrorAttribute 特性即可。接着重写基类的 OnException 虚方法。代码如下:
public class CustomHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext Context)
{
base.OnException(Context);
dynamic ex = Context.Exception;
if (!Context.ExceptionHandled)
return;
//TODO:将 ex 错误对象记录到系统日志模块
}
}
接着我们在MVC Controller 内这样调用:
/*
*截获InvokeActionResult 调用 actionResult参数的 ExecuteResult 方法。
ExecuteResult 方法执行时 进行 以下操作:
JavaScriptSerializer serializer = new JavaScriptSerializer();
response.Write(serializer.Serialize(this.Data));
将C# 匿名对象序列化成json数据供 jquery ajax 方法回调。
*/
[CustomHandleError]
public JsonResult SaveTest()
{
//try
//{
//TestMehod();
//LinqToSql:返回IQueryable数据集合。
var iQueryableData = (from o in _Context.Orders//.Where(o => o.OrderID == 10248)
select new
{
ShipName = o.ShipName,
Employee = o.Employee,
}).ToList();
//LINQ:返回IEnumerable集合。
var iEnumerableData = from d in iQueryableData
select new
{
ShipName = d.ShipName,
EmployeeName = d.Employee.LastName //空引用未处理引发不可截获的异常。
};
return Json(new { Success = true, Msg = iEnumerableData }, JsonRequestBehavior.AllowGet); //Json序列化。
//}
//catch //外层错误,导致内层函数catch失效,无法有效的截取错误信息。
//{
// return Json(new { Success = false, Msg = "操作失败。" }, JsonRequestBehavior.AllowGet);
//}
}
代码看上去更简洁了吧?连try catch 都不用写了更不用担心啥时候这个截获不到的exception神秘人物登场,这样噩梦结束了。
本例代码这里下载