MVC Error Handle with custom error Messages

15,884

Solution 1

Your requirement best fit with Elmah. Very good plugin for logging errors.

ELMAH stands for Error Logging Modules And Handlers

ELMAH provides such a high degree of plugability that even Installation of ELMAH does not require compilation of your application.

ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. It can be dynamically added to a running ASP.NET web application, or even all ASP.NET web applications on a machine, without any need for re-compilation or re-deployment.

Reference from the blog of SCOTT HANSELMAN

Just need to copy binary of ELMAH to bin folder of your application and edit web.config file. That's it!

you need to add following to your web.config and make some other changes described in the following links.

<sectionGroup name="elmah">
  <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
  <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
  <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
  <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
</sectionGroup>

For example to set up mail account.

<configuration>
    <configSections>
        <sectionGroup name="elmah">
            <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
            <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
            <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
        </sectionGroup>
    </configSections>
    <elmah>
    <errorMail from="[email protected]" to="[email protected]"
       subject="Application Exception" async="false"
       smtpPort="25" smtpServer="***"
       userName="***" password="***">
    </errorMail>
    </elmah>
<system.web>        
    <customErrors mode="RemoteOnly" defaultRedirect="CustomError.aspx">
        <error statusCode="403" redirect="NotAuthorized.aspx" />
        <!--<error statusCode="404" redirect="FileNotFound.htm" />-->
    </customErrors>
    <httpHandlers>
        <remove verb="*" path="*.asmx"/>
        <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
    </httpHandlers>
    <httpModules>
        <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
        <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
    </httpModules>
</system.web>
</configuration>

Here is some good reference link (that contains detailed reference to installation of ELMAH to your project) for your reference.

https://msdn.microsoft.com/en-us/library/aa479332.aspx

https://code.google.com/p/elmah/wiki/MVC

Update

Add to the detail custom information (for example the Id of the record I'm trying to read)

You can build your own custom exception that derives from Exception.

public class MyException : Exception
{
    public MyException(string message, Exception ex) : base(ex.Message, ex)
    {

    }
}

and then using it like

public virtual ActionResult Index()
{
    try
    {
        return View();
    }
    catch (Exception e)
    {
        throw new MyException("detailed exception", e);
    }
}

in this way main exception would be wrapped inside the myexception and you can add your detailed custom exception message.

Return to the view custom messages to the user

You just need to add

<system.web>
    <customErrors mode="On">
    </customErrors>
<sytem.web>

and add Error.cshtml inside the ~/View/Shared folder Then whenever exception is encountered it will find for Error.cshtml inside view/shared folder and render the content. so you can render there your custom message.

Solution 2

Use Elmah as others have also recommended. I am, and haven't looked back!

It meets all your requirements:

  • Catches all errors, e.g. 400s, 500s...
  • Logs to a file, and any other data store you can think of, e.g. database, memory, Azure, more file formats(XML, CSV), RSS feed...
  • Emails errors: Enable and config mail settings in Web.config - very simple. You can even send emails asynchronously!
  • Add custom code - in your case add extra details to the error
  • Use your own custom error pages - custom error node (for 400s, 500s) in web.config and your own error controller

Further on the custom code (2nd last point above), AFAIK you have two options:

1. Create a custom error log implementation.

This isn't that difficult. It's what I did!

Override the default error log data store. For example, taking the SQL Server data store:

In Web.config
<elmah>
   <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="myCn" applicationName="myAppName" />
</elmah>

Next, create a class "MySQLServerErrorLog" and derive from Elmah.ErrorLog

All that is then required is to override the Log() method.

public override string Log(Error error)
        {   

        // You have access to all the error details - joy!      
        = error.HostName,
        = error.Type,
        = error.Message,
        = error.StatusCode
        = error.User,
        = error.Source,

        // Call the base implementation
    }

In Web.config, replace the default (above) entry with your implementation:

<elmah>
   <errorLog type="myProjectName.MySQLServerErrorLog, myProjectName" />
</elmah>

2. You can programmatically log errors

Using the ErrorSignal class, you may logs errors without having to raise unhandled exceptions.

Syntax: ErrorSignal.FromCurrentContext().Raise(new NotSupportedException());

Example: A custom exception

var customException = new Exception("My error", new NotSupportedException()); 
ErrorSignal.FromCurrentContext().Raise(customException);

This gives you the option of using your custom logic to programmatically log whatever you require.

I've written functionality for my Elmah instance to logs errors to Azure Cloud Storage Table and Blob (error stack trace details).

FWIW before I used Elmah, I had written my own exception handling mechanism for MVC which used HandleErrorAttribute and Application_Error (in Global.asax). It worked but was too unwieldy IMO.

Solution 3

If it was me, I'd create my own exception handling Attribute which adds required behaviour to the base implementation of HandleErrorAttribute.

I've had quite good results in the past with having attributes "pointed at" various parts of the request that's of interest (am thinking the bit where you say that you want to log specific details) - so you can use these identifiers to pull the request to pieces using reflection:

CustomHandleErrorAttribute(["id", "name", "model.lastUpdatedDate"])

I've used this approach to secure controller actions (making sure that a customer is requesting things that they're allowed to request) - e.g. a parent is requesting info on their children, and not someone else's children.

Or, you could have a configuration set up whereby you'd "chain" handlers together - so lots of little handlers, all doing very specific bits, all working on the same request and request pointers (as above):

ChainedErrorHandling("emailAndLogFile", ["id", "name", "model.lastUpdatedDate"])

Where "emailAndLogFile" creates a chain of error handlers that inherit from FilterAttribute, the last of which, in the chain, is the standard MVC HandleErrorAttribute.

But by far, the simplest approach would be the former of these 2.

HTH


EDITED TO ADD: Example of inheriting custom error handling:

public class CustomErrorAttribute : HandleErrorAttribute
{
    public CustomErrorAttribute(string[] requestPropertiesToLog)
    {
        this.requestPropertiesToLog = requestPropertiesToLog;
    }

    public string[] requestPropertiesToLog { get; set; }

    public override void OnException(ExceptionContext filterContext)
    {
        var requestDetails = this.GetPropertiesFromRequest(filterContext);

        // do custom logging / handling
        LogExceptionToEmail(requestDetails, filterContext);
        LogExceptionToFile(requestDetails, filterContext);
        LogExceptionToElseWhere(requestDetails, filterContext);// you get the idea

        // even better - you could use DI (as you're in MVC at this point) to resolve the custom logging and log from there.
        //var logger = DependencyResolver.Current.GetService<IMyCustomErrorLoggingHandler>();
        // logger.HandleException(requestDetails, filterContext);

        // then let the base error handling do it's thang.
        base.OnException(filterContext);
    }

    private IEnumerable<KeyValuePair<string, string>> GetPropertiesFromRequest(ExceptionContext filterContext)
    {
        // in requestContext is the queryString, form, user, route data - cherry pick bits out using the this.requestPropertiesToLog and some simple mechanism you like
        var requestContext = filterContext.RequestContext;
        var qs = requestContext.HttpContext.Request.QueryString;
        var form = requestContext.HttpContext.Request.Form;
        var user = requestContext.HttpContext.User;
        var routeDataOfActionThatThrew = requestContext.RouteData;

        yield break;// just break as I'm not implementing it.
    }

    private void LogExceptionToEmail(IEnumerable<KeyValuePair<string, string>> requestDetails, ExceptionContext filterContext)
    {
        // send emails here
    }

    private void LogExceptionToFile(IEnumerable<KeyValuePair<string, string>> requestDetails, ExceptionContext filterContext)
    {
        // log to files
    }

    private void LogExceptionToElseWhere(IEnumerable<KeyValuePair<string, string>> requestDetails, ExceptionContext filterContext)
    {
        // send cash to me via paypal everytime you get an exception ;)
    }
}

And On the controller action you'd add something like:

[CustomErrorAttribute(new[] { "formProperty1", "formProperty2" })]
public ActionResult Index(){
    return View();
}

Solution 4

Firstly, you can define a filter attribute, and you can register it on startup in an MVC application in global.asax, so you can catch any kind of errors that occur while actions are invoking.

Note: Dependency Resolving is changeable. I'm using Castle Windsor for this story. You can resolve dependencies your own IOC container. For example, ILogger dependency. I used for this property injection while action invoking. Windsor Action Invoker

For Example Filter:

public class ExceptionHandling : FilterAttribute, IExceptionFilter
{
    public ILogger Logger { get; set; }

    public void OnException(ExceptionContext filterContext)
    {
        Logger.Log("On Exception !", LogType.Debug, filterContext.Exception);

        if (filterContext.Exception is UnauthorizedAccessException)
        {
            filterContext.Result = UnauthorizedAccessExceptionResult(filterContext);
        }
        else if (filterContext.Exception is BusinessException)
        {
            filterContext.Result = BusinessExceptionResult(filterContext);
        }
        else
        {
            // Unhandled Exception
            Logger.Log("Unhandled Exception ", LogType.Error, filterContext.Exception);
            filterContext.Result = UnhandledExceptionResult(filterContext);
        }
    } 
}

This way you can catch everything.

So:

private static ActionResult UnauthorizedAccessExceptionResult(ExceptionContext filterContext)
{
    // Send email, fire event, add error messages 
    // for example handle error messages
    // You can seperate the behaviour by: if (filterContext.HttpContext.Request.IsAjaxRequest())
    filterContext.ExceptionHandled = true;
    filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
    filterContext.Controller.TempData.Add(MessageType.Danger.ToString(), filterContext.Exception.Message);

    // So you can show messages using with TempData["Key"] on your action or views
    var lRoutes = new RouteValueDictionary(
        new
        {
            action = filterContext.RouteData.Values["action"],
            controller = filterContext.RouteData.Values["controller"]
        });
    return new RedirectToRouteResult(lRoutes);
}

In Global.asax:

protected void Application_Start()
{
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
}

FilterConfig:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new ExceptionHandling());
}

BusinessException:

public class BusinessException : Exception, ISerializable
{
    public BusinessException(string message)
        : base(message)
    {
        // Add implemenation (if required)
    }
}

So you can access the exception message OnException at ExceptionHandling class with filterContext.Exception.Message

You should use BusinessException on the action after any violated control logic this way: throw new BusinessException("Message").

Share:
15,884

Related videos on Youtube

Patrick
Author by

Patrick

Devoting most of my time developing web applications, I realized over the years that the common web developer is too focus on improvements in design, development and implementation of internet applications, often forgetting the most important, the user . With the user in mind, the project was born http://www.izigo.pt . The project aims to provide a search engine to search for services and vehicles in the automotive sector, in a simple, fast and geo referenced ( Research nearest garage service for example), always trying to provide updated information using the latest information technologies available in the market . izigo.pt intends to organize and deliver high quality content in the automotive sector where the automobile market through workshops , booths , business directory , services and dealers will be referenced and accessible . Our partners will have increased visibility in a complex area that requires great trust between suppliers and customers and in which the information is often not easily accessible . Please check my LinkedIn profile for aditional information: http://pt.linkedin.com/in/patricksardinha/

Updated on June 04, 2022

Comments

  • Patrick
    Patrick almost 2 years

    I'm building a new Web Application using MVC5 and I need the followings:

    1. Catch errors
    2. Log the details in a file
    3. Send them by email
    4. Add to the detail custom information (for example the Id of the record I'm trying to read)
    5. Return to the view custom messages to the user

    I have found a lot of information regarding the HandleErrorAttribute but none of them allow to add specific details to the error, also I have found information saying that the try catch aproach is too heavy for the server.

    For now, I have:

    Controller:

    public partial class HomeController : Controller
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();
    
        public virtual ActionResult Index()
        {
            try
            {
                return View();
            }
            catch (Exception e)
            {
                logger.Error("Error in Index: " + e);
                return MVC.Error.Index("Error in Home Controller");
            }
        }
    }
    

    I have found this Extended HandleErrorAttribute that seems complete but don't do everything I need:

    private bool IsAjax(ExceptionContext filterContext)
    {
        return filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
    }
    
    public override void OnException(ExceptionContext filterContext)
    {
        if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
        {
            return;
        }
    
    
        // if the request is AJAX return JSON else view.
        if (IsAjax(filterContext))
        {
            //Because its a exception raised after ajax invocation
            //Lets return Json
            filterContext.Result = new JsonResult(){Data=filterContext.Exception.Message,
                JsonRequestBehavior=JsonRequestBehavior.AllowGet};
    
            filterContext.ExceptionHandled = true;
            filterContext.HttpContext.Response.Clear();    
        }
        else
        {
            //Normal Exception
            //So let it handle by its default ways.
            base.OnException(filterContext);
    
        }
    
        // Write error logging code here if you wish.
    
        //if want to get different of the request
        //var currentController = (string)filterContext.RouteData.Values["controller"];
        //var currentActionName = (string)filterContext.RouteData.Values["action"];
    }
    
    • Daniel J.G.
      Daniel J.G. about 9 years
      Have you looked into Elmah?
  • Patrick
    Patrick about 9 years
    Hi thanks Andrew, but I'm having some dificulties understanding the concept. Any example you could give to undertsnd it better?
  • Patrick
    Patrick about 9 years
    Hi thanks Thomas, any reference to implement Elmah with custom code to add adicional information in MVC?
  • Patrick
    Patrick about 9 years
    Hi thanks Jenish, any example to add custom messages?
  • OpcodePete
    OpcodePete about 9 years
    Best to use NuGet Elmah.MVC (and dependency Elmah.CoreLibrary) packages. It will copy the binaries and Web.config entries.
  • Andrew Hewitt
    Andrew Hewitt about 9 years
    Hi Patrick. Sorry - on re-read I can see why it's not clear. So - we're saying that the standard HandleErrorAttribute does some, but not all of the required actions - what I propose (and have done in similar circumstances with great success) is creating a new attribute class that inherits from this one, and uses the base implementation once it does it's extra stuff (i.e. logging to file / email whilst gaining details from the request on a case by case basis). I'll add an extra answer with a pseudo example, maybe that will help.
  • Andrew Hewitt
    Andrew Hewitt about 9 years
    Hi Patrick - have edited my answer to give you a start with the attribute. HTH!
  • Andrew Hewitt
    Andrew Hewitt about 9 years
    Hey - np Patrick - good luck! if you get stuck, give me a shout :)
  • Patrick
    Patrick about 9 years
    Hi, Thanks! Where do you pass custom information from the Action to the Filter, for example, a ClientId?
  • Oğuzhan Soykan
    Oğuzhan Soykan about 9 years
    No, you can define a Exception class like as BusinessException class.(Note: BusinessException class should be derived from System.Exception) So, when you type new BusinessException("Parameter is invalid"); on the action method, the businessexception transmit to OnException event, so you can handle this way.
  • Patrick
    Patrick about 9 years
    And how do you implement it in a common Controller/Action when an error occurs and you want to pass the ClientId that generated the error for example?
  • Oğuzhan Soykan
    Oğuzhan Soykan about 9 years
    If I understood correctly, you show ClientID with Error Messages or you want to Log ClientId. You can pass json string to message with ClientId, and deserialize it OnException event ?
  • Patrick
    Patrick about 9 years
    I want to log ClientId with other informations (other status variables), the place where it hapened (Controller/Action) and to be able to respond properly to the user regarding the call (Post/Get/Json). This way, I can receive the error and check what hapened and send a friendly message to the user.