在ASP.NET MVC中处理错误和异常

10
什么是ASP.NET MVC结构中处理错误和捕获异常的通用方法? 普遍共识是让异常冒泡。那么哪一层(视图或控制器)会处理异常(捕获/显示用户友好文本等)?我的直觉是在控制器中完成? 编辑: 我想避免在每个控制器操作中重复编写相同的错误处理代码,因此我正在寻找如何实现错误处理而不重复相同代码的简洁示例。

1
使用拦截技术能够在这里真正有所帮助。许多依赖注入容器都支持此功能。我知道Spring.NET和Castle Windsor两者都与asp.net mvc集成。您将拦截对控制器的调用并在拦截器中处理异常。 - Marijn
这个问题可以作为一个良好的起点:https://dev59.com/KVXTa4cB1Zd3GeqP3Jp- - Marijn
这是我最终做的事情:http://stackoverflow.com/questions/8249479/getting-properties-from-jsonresult-on-the-js-side-inside-jquery-ajax - sarsnake
7个回答

17

在控制器中处理异常可能会导致大量的重复代码。更好的方法是在扩展HandleErrorAttribute的操作筛选器中处理异常。在那里,您可以记录异常,然后重定向到显示友好消息指示用户出现问题的页面。

但是,在某些情况下,您需要在控制器方法中处理异常,例如,当您可以从异常中恢复并向用户显示适当的消息时,例如您的业务层引发的异常表示提供的值无效。在这种情况下,您应该捕获特定的异常并向用户显示相同的视图以及适当的消息。

编辑:

public class CustomErrorHandlerAttribute : HandleErrorAttribute
{
     public override void OnException(ExceptionContext filterContext)
     {
         var logger = log4net.LogManager.GetLogger("SomeLoggerHere");

         logger.Error("An unhandled error occurred", filterContext.Exception);

         if (filterContext.HttpContext.Request.IsAjaxRequest())
         {
             filterContext.HttpContext.Response.Clear();
             filterContext.HttpContext.Response.Status = "500 Internal Server Error";
             filterContext.Result = new JsonResult { Data = new { ErrorMessage = filterContext.Exception.Message } };
             filterContext.ExceptionHandled = true;                
         }
         else
         {
             base.OnException(filterContext);
         }

    }

编辑2: 然后你可以像这样使用该属性:

[CustomErrorHandler]
public class AnyController : Controller
{
...
}

你能提供一个清晰的示例来展示如何使用HandleErrorAttribute实现错误处理吗?谢谢。 - sarsnake
谢谢,那么CustomErrorHandlerAttribute本身放在哪里?同一个文件吗?这有关系吗? - sarsnake
如果你想的话,可以在不同的目录中创建不同的类(例如Filters)。 - uvita
谢谢,我会试一下。由于今天可能没有时间尝试,我会延长悬赏。 - sarsnake
我会给你赏金,我最终做了类似的事情,但还扩展它来处理JsonResult,这样我就可以将自定义错误消息返回给我的js代码。 - sarsnake
显示剩余2条评论

3
您的直觉是正确的。这绝对需要是控制器。以下是一个例子:
[HttpPost]
public ActionResult Create(OrderViewModel model)
{
   if (!ModelState.IsValid)
     return View(model);

   try
   {
      repository.Save(model);
      unitOfWork.Commit();
      return RedirectToAction("Index");
   }
   catch (Exception exc)
   {
      _loggingService.Error(exc);
      ModelState.AddModelError("KeyUsedInView", exc.Message); // or, show a generic error.
   }

   return View(model);
}

注意:

  • 先检查ModelState。如果无效,直接返回。这样可以省去很多麻烦。
  • 不要反复实例化日志记录服务。使用单例模式,并使用DI将其注入到控制器中,以便使用接口,例如ILoggingService。这还意味着您可以将其他功能添加到日志记录服务中(例如向支持部门发送电子邮件)。
  • 较低层(服务、存储库等)可能会抛出错误(自定义或内置),因此控制器捕获它们非常重要,因为它是“聚合器”,负责客户端和服务器之间的流程。
  • 使用ModelState.AddModelError添加错误,以便视图可以显示它们。您还可以使用自定义异常,这些异常可能更友好,并且可以向用户显示它们。对于较低级别的错误(SQL等),可以在ModelState中添加通用错误(“对不起,发生了错误,请稍后再试”)。

在这种情况下,你如何保存用户名?还有如何创建所有发生错误的报告? - Amir978
@Amir978 - 我不明白。他在问题中哪里要求做这些事情? - RPM1984
@Amir978 - 没问题。不确定为什么您要“保存用户名” - 是什么情况?注册?也许可以再发一个问题。至于错误报告 - 这就是 Elmah 的作用。而且您可以使用类似 Sentinel 的工具来捕获日志错误。 - RPM1984

2

这并不像看起来那么容易。

如果您需要集中式异常处理,最快的方法是在控制器中重写OnException方法。

[NonAction]
        protected override void OnException(ExceptionContext filterContext)
        {

            this.Session["ErrorException"] = filterContext.Exception;

            if (filterContext.Exception.GetType() == typeof(PEDException))
            {
                // Mark exception as handled
                filterContext.ExceptionHandled = true;

                // ... logging, etc

                // Redirect
                filterContext.Result = this.RedirectToAction( "ShowError", "Errors");
            }

            base.OnException(filterContext);
        }

从这个方法中可以看到,我已经捕获了所有未处理的PEDException异常,如果您的bl引发自定义异常,我认为拥有一个带有OnException方法的基本控制器可能是一个很好的解决方案,但是在某些情况下,这可能潜在地危险。通常来说,最好定义一个定制的属性(扩展ErrorAttributeFilter),以避免出现许多其他问题(例如缓存问题,您的操作根本不会执行,而属性始终会执行)。

请参见此处获取更多信息。


1
你可以创建一个自定义的基础控制器并继承Base Controller类。然后在你的自定义控制器中重写OnException方法。接着让每个控制器都继承你的新自定义基础控制器。
另外,你也可以在global.asax中重写Application_Error事件。

0

虽然我同意将异常记录操作集中在基础控制器级别以保持一致性,但我认为应该考虑一种标准的方式来显示用户友好型的UI错误消息,并与记录要求相结合。在控制器级别捕获并记录的异常可以被包装成“友好异常”,然后传递给一个中央异常处理器。共享视图文件夹中的error.cshtml文件是处理和显示包装了在控制器级别发生并已记录下来的“实际异常”的“友好异常”的好地方。

我正在使用自定义基础控件并设置HandleErrorAttribute,通过从Global.asax Application_Start事件调用FilterConfig类中的RegisterGolbalFilters方法。

Base_Controller代码

public class Base_Controller : Controller
{
    protected override void OnException(ExceptionContext filterContext)
    {
        Exception e = filterContext.Exception;
        //Custom Exception Logging Here
        //Log Exception e
        //Elmah.Mvc.ElmahController ec = new Elmah.Mvc.ElmahController();
        base.OnException(filterContext);
    }
}

FilterConfig.cs 代码

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
    }
}

Global.asax 应用程序启动代码

void Application_Start(object sender, EventArgs e)
{
    // Code that runs on application startup
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

}

那部分是相当标准的... 接下来你所做的才真正对用户有所影响。 我使用一个错误处理类来集中处理异常,并根据控制器和操作显示基于上下文的“友好”消息。在下面的示例代码中,我将catch块移动到Views/Shared文件夹中的error.cshtml页面中,以保持简单。下面的代码只是示例代码,因为友好的错误消息将根据应用程序上下文而改变,您可能希望将异常处理移入类中以便于维护。

//Check for Transport Exception with "Actual Exception" stored
//in the inner exception property
if (Model.Exception.InnerException != null)
{
    errFriendly = Model.Exception.Message;
    modelEx = Model.Exception.InnerException;
}
else
{
    modelEx = Model.Exception;
}
try
{           
    throw modelEx; 
}
catch (System.Data.SqlClient.SqlException ex)
{
    //Display Landing page friendly error for exception caused by home controller
    //Display generic data access error for all other controllers/actions
    if (Model.ActionName == "Index" && Model.ControllerName == "Home")
    {errFriendly = "Landing page cannot display product data...";}
    else
    {errFriendly = "Problem Accessing Data...";}
    errType = ex.GetType().ToString();
    errActual = ex.Message;
}

如需下载完整的代码示例,请参阅博客文章:http://www.prodataman.com/Blog/Post/119/MVC-Custom-Exception-Handling


0
通常我会在Global.asax文件中重写Application_Error方法,并为每个异常重定向用户到通用的异常页面,然后发送带有一些细节的电子邮件。这很简单。以下是我通常使用的代码:
protected void Application_Error(object sender, EventArgs e)
{
    if (Request.Url.ToString().StartsWith("http://localhost:"))
        return;
    string msg;
    Exception ex = Server.GetLastError().GetBaseException();
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("Exception Found");
    sb.AppendLine("Timestamp: " + System.DateTime.Now.ToString());
    sb.AppendLine("Error in: " + Request.Url.ToString());
    sb.AppendLine("Browser Version: " + Request.UserAgent.ToString());
    sb.AppendLine("User IP: " + Request.UserHostAddress.ToString());
    sb.AppendLine("Error Message: " + ex.Message);
    sb.AppendLine("Stack Trace: " + ex.StackTrace);
    msg = sb.ToString();
    Server.ClearError();
    YourMailHelper.SendException("Your Site Exception", msg);
    Response.Redirect("~/Error.html");
}

0

这真的取决于你想要实现什么。

对于简单的场景,比如在任何错误情况下显示自定义消息,你可以在 web.config 中使用老式的自定义错误配置。

请注意,这将用于甚至没有到达控制器的错误。比如当 URL 中存在未正确编码的特殊值时。

HandleError 属性或您自己的自定义属性,允许您在其他场景中获得更精细的控制。

请注意,如果您想将自定义处理错误属性应用于所有控制器,可以将其作为全局操作筛选器应用。这样,您就不需要显式地将其应用于每个控制器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接