Friday, June 1, 2012

MVC Error page linked with Elmah error code

Recently in a lot of projects I've seen the popularity of using elmah error logger, even more so than log4net, its comfortable, easy to configure and very simple to follow errors. 


If you're developing a business application and want to provide serious support to your customers you'll prefer to give your users some way of describing the error, so avoiding generic error pages is a good practice but you don't want to give them a window into your application, it might open a serious security risk, using elmah's error id is the best option in my opinion, since it gives you the ability to see what went wrong, combine that with either asking the user what they tried to do or incorporating into the exception the data passed into the failing method/action and you have a better way of solving problems.


After digging around, I've seen this, which gave me a lead, so thanks for the info! 


Here's the simplest way of doing it (there's a demo project at the end of this post):


1. Add Elmah.Contrib.Mvc, you can do it via the NuGet packages.


2. Change your web.config to persist the errors, disk/sql or what ever you like, this way the id will mean something if the application is restarted, because the default is memory.


3. Set custom errors in web.config, for example: 
<customErrors mode="On" defaultRedirect="~/Demo/ShowError" />


4. If you want all the errors to be caught by elmah, you'll have to comment this line in global.asax -> void RegisterGlobalFilters(GlobalFilterCollection filters):
 filters.Add(new HandleErrorAttribute());


The default error handler doesn't alert elmah when an exception is thrown and custom errors are on.


5. Add this to your global.asax, its for elmah to handle your errors:


protected void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
        {
            var result = ElmahErrorHandling.ProcessError(sender, args);
            if (!string.IsNullOrEmpty(result))
                Response.Redirect(result);
        }


6. Insert this class in your project:


/// <summary>
    /// Elmah Error handling helper class
    /// </summary>
    public class ElmahErrorHandling
    {
        /// <summary>
        /// Configuration
        /// </summary>
        public static Configuration config = WebConfigurationManager.OpenWebConfiguration("~");

        /// <summary>
        /// CustomErrors configuration section
        /// </summary>
        public static CustomErrorsSection customErrorsSection = (CustomErrorsSection)config.GetSection("system.web/customErrors");

        /// <summary>
        /// Handlers configuration section
        /// </summary>
        public static HttpHandlersSection handlers = (HttpHandlersSection)config.GetSection("system.web/httpHandlers");

        /// <summary>
        /// Process errors caught in Global.asax
        /// <para>should be inside protected void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)</para>
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns>null if no custom error should be displayed or a link if redirect is required to the specific location</returns>
        public static string ProcessError(object sender, ErrorLoggedEventArgs args)
        {
            if (customErrorsSection != null)
            {
                switch (customErrorsSection.Mode)
                {
                    case CustomErrorsMode.Off:
                        break;
                    case CustomErrorsMode.On:
                        return customErrorsSection.DefaultRedirect + "?id=" + args.Entry.Id;
                    case CustomErrorsMode.RemoteOnly:
                        if (!HttpContext.Current.Request.IsLocal)
                            return customErrorsSection.DefaultRedirect + "?id=" + args.Entry.Id;
                        break;
                }
            }

            return null;
        }

        /// <summary>
        /// Get path to Elmah error handler
        /// <para>e.g. elmah.axd</para>
        /// </summary>
        /// <returns></returns>
        public static string GetElmahHandler()
        {
            if ((handlers != null) && (handlers.Handlers != null))
                for (int i = 0; i < handlers.Handlers.Count; i++)
                    if (handlers.Handlers[i].Type.IndexOf("elmah", StringComparison.InvariantCultureIgnoreCase) != -1)
                        return handlers.Handlers[i].Path;
                
            return null;
        }

        /// <summary>
        /// Gets absolute path to elmah error log
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static string GetHandlerLink(string id)
        {
            return VirtualPathUtility.ToAbsolute(string.Format("~/{0}/detail?id={1}",GetElmahHandler(), id));
        }

        /// <summary>
        /// Gets Error string according to configuration and id
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static string GetErrorString(string id)
        {
            if (customErrorsSection != null)
            {
                switch (customErrorsSection.Mode)
                {
                    case CustomErrorsMode.Off:
                        return string.Format("<a href='{0}'>{1}</a>",GetHandlerLink(id),id);
                    case CustomErrorsMode.On:
                    case CustomErrorsMode.RemoteOnly:
                        if (HttpContext.Current.Request.IsLocal)
                            return string.Format("<a href='{0}'>{1}</a>", GetHandlerLink(id),id);
                        else
                            return id;
                }
            }
            return string.Empty;
        }

        /// <summary>
        /// returns MvcHtmlString for error link/text
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static MvcHtmlString GetError(string id)
        {
            return new MvcHtmlString(GetErrorString(id));
        }
    }



7. Create your error page, make sure its the correct path in customErrors section in your web.config and add this:
@ElmahErrorHandling.GetError(ViewBag.id)


It will either display a link to elmah.axd or an id, depending on your configuration.




There's a demo project at:
https://github.com/drorgl/ForBlog/tree/master/MvcElmahErrorDemo

No comments:

Post a Comment