简体   繁体   中英

Handling errors and exceptions in asp.net mvc

What is the accepted way to handling errors and trapping exception in asp.net mvc structure? The general consensus is to have exceptions bubble up. So what layer (View or Controller) would you handle the exceptions (catch them/ display user friendly text, etc). My hunch is that it is done in the controller? EDITED: I would like to avoid repeating the same error handling code in every controller action ..hence I am looking for a concise example of how to implement error handling without repeating the same code.

Handling exceptions in the controller could lead to a lot of repetitive code. A better approach is to handle them in an action filter that extends HandleErrorAttribute . There you could log exceptions and then redirect to a page that displays a nice message indicating the user that something went wrong.

However, there are some cases where you need to handle exceptions in your controller methods, for example, when you can recover from an exception and show the user an appropriate message, for example an exception that was throw by your business layer indicating that the values you provided are not valid. In that case, you should catch the specific exception and show the user the same view along with an appropriate message.

EDIT:

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);
         }

    }

}

EDIT 2: Then you use the attribute like this:

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

Your hunch is correct. Definetely needs to be the controller. Here's an example:

[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);
}

Notes:

  • Check ModelState first. If not valid, return. Get's it out of the way.
  • Don't keep newing up a logging service. Use a singleton instance, and use DI to inject it into your controllers so your working off an interface, eg ILoggingService . This also means you can add other functionality into the logging service (emailing support, for example).
  • Your lower layers (services, repository, etc) may throw errors (custom, or built-in), so it's important the controller catches them, as it's the "aggregator" and what is responsible for the flow between the client and the server.
  • Use ModelState.AddModelError to add errors so the View can show them. You can also use custom exceptions which may be user friendly and you show them to the user. For lower-level errors (SQL, etc), you can simply add a generic error to the ModelState ("Sorry, an error occured. Please try again later").

It's not so easy as it could seem.

If you need centralized exception handling, fastest way is to override the OnException method in the Controller

[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);
        }

As you can see in this method I've catched all unhandled PEDException exceptions, if you have custom exception raised from your bl, I think having a base controller with an OnException method can be a good solution, however there are cases where this can potentially be dangerous. Generally I think that it's better to define a custom Attribute (Extending ErrorAttributeFilter) to avoid a lot of other problems (with caching for example, your actions simply won't be executed at all, while the attribute will always be executed).

Please see here for further informations

You could create a custom base controller and inherit from the Base Controller class. Then override the OnException in your customcontroller. Then have each of your controller's inherit from your new custom base controlller.

Alternatively, you can override the Application_Error event in the global.asax

While I agree that exception logging operations should be centralized at the BASE controller level for the sake of consistency... I also believe that a standard way of displaying user friendly error messages in the UI should be considered in addition to logging requirements. Exceptions captured and logged at the Controller level can then be wrapped in a "Friendly Exception" and passed to a central exception processor. The error.cshtml file in the shared view folder is a good place to process and display the "Friendly Exception" that wraps the "Actual Exception" that occurred and was logged at the Controller level and passed on to the error.cshtml.

I'm using a custom base control and setting the HandleErrorAttribute using the RegisterGolbalFilters method in the FilterConfig class called from the Global.asax Application_Start event

Base_Controller Code

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 Code

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

Global.asax Application_Start Code

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);

}

That part is pretty standard... Its what you do next that really makes a difference to the user. I use an errorhandling class to process exceptions centrally and display "Friendly" context based messages to the user based on the Controller and Action that caused the exception. In the sample code below I have moved the catch block to the error.cshtml page in the Views/Shared folder to keep it simple. The code below is just sample code as the friendly error message will change based on application context and you may want to move exception processing into class for the sake of maintenance.

//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;
}

To download the entire code sample see the blog post at: http://www.prodataman.com/Blog/Post/119/MVC-Custom-Exception-Handling

It really depends on what you're aiming to achieve.

For the simple scenario of displaying a custom message on any errors all around, you can use the good old custom errors configuration in your web.config.

Note that this will be used on errors that don't even get to the point of reaching the controllers. Like when there are problems with not properly encoded special values in URLs.

The HandleError attribute, or your own custom attribute, allow you a finer control that you want want in other scenarios.

Note that if you want to apply your custom handle error attribute to All controllers, you can do so by applying it as a global action filter. That way you don't need to explicitly apply it to each and every controller.

I usually override Application_Error in the Global.asax file and redirect the user to a generic exception page for every exception, and then send an e-mail with some details. Simple enough. Here's what I usually use:

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");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM