ASP.NET Exception Handler Redux

I have always been reluctant to get involved with some of these "Best Practices" code frameworks that are foisted on developers. I've found that often you can end up with a lot of extra overhead and functionality that you don't even need. One example for me is Enterprise Library. There are constant Feature CTP's, RC's, new builds. And while it is possible to use the Wizard to configure most things "out of the box", it is not always easy.

In addition, the default setup for the Exception Handling block with database logging does not provide you with many of the things you would expect to log for an ASP.NET application - Page Name, HTTP Referer, the Form collection, Querystring, and even the StackTrace which would provide you with line numbers in your code. None of these are in there in the default options, and there is no easy way to add them.  You can extend the logging templates and even the source code - but its not easy. Getting a revised build to "take" what with the strong naming and everything else is a real challenge.  Entire communities have sprung up around Enterprise Library with hundreds of forum posts asking for help on this and that.  I say, it should not have to be so complex. I'm just a minimalist, I guess. I believe that in coding, "less is more". Or to put it the way Einstein said, "Everything should be as simple as possible, but no simpler".

For a number of years I've used a "roll your own" Exception handler library that is very small, logs to a SQL Server table, and optionally sends out emails whenever there is an exception. I have an Admin "Report.aspx" page in my apps that lets an Admin look at the exception logs in a nice GridView, and even search the LogItems database table based on a number of inputs. It requires no special web.config sections, doesn't need a wizard, and is very fast. It also uses the original Microsoft v2 Data Access "SqlHelper" class, which is very fast and has stood the test of time. The only thing I've added to that is a static CommandTimeout field that has a default value of 180. The SqlHelper class caches SqlParameters; Enterprise Library does not seem to implement this valuable feature.  SqlHelper requires nothing but a connection string.

Originally (this is back in 2001) I had this wired up to send Syslog messages also, but I haven't used that feature in so long that I took it out. Again, "less is more".

One thing I did like about Enterprise Library's Exception Block is the way you can wire up method calls with their ExceptionManager class "Process" method, like this:

     DataSet ds=   ExceptionManager.Process(() => TestDataSet(ID,email), "ExceptionShielding");

What happens with the above is that if no exception is thrown, you get your TResult ( in this case a DataSet). If an exception is thrown, it is logged and you get the default value of TResult (usually, null).

So I added an ExceptionManager class that offers that new convenience feature.  Of course you can also do logging "the old way", like this:

  try
      {
         // your buggy code here
      }
  catch(Exception ex)
     {
      ExceptionLogger.HandleException(ex);
     }


I originally set this up with appSettings elements, and I see no reason to change it because it is ultra-simple:

   <add key="LogExceptions" value="true"/>    
    <add key="exceptionLogConnString" value="server=(local);database=Exceptions;Integrated Security=SSPI"/>
    <!-- semicolon delimited list of email addresses to receive exception emails. If empty, does not send any.-->
    <add key="emailAddresses" value=""/>
     <add key="smtpServer" value="mail.yourdomain.com"/>
    <add key="fromEmail" value="info@yourdomain.com"/>
    <add key="detailURL" value="http://yourdomain.com/Admin/report.aspx"/>  
    <add key="smtpUserName" value="you@yourdomain.com"/>
    <add key="smtpPassword" value="yourpassword"/>    
  </appSettings>

I kept the email setttings separate from the default System.Net mail config settings because I reasoned that you might have a separate email "setup" for logging exceptions - different from your regular settings.

If there are any email addresses entered in the "emallAddresses" element, the handler will automatically email to each of them with a link to the report page and the exact exception "ID" on the QueryString. Otherwise, it sends no email, only logging the exception to the database. You can also easily turn off logging by setting the "LogExceptions" element value to "false".

I know that there are some other attempts at .NET exception logging (the one I hear about most often is ELMAH) but mine does everything I need, so I haven't even bothered to look at them. Plus, if I decide I want to enhance it, the code is so simple that it's a snap.

So in this demo I present my complete PAB.ExceptionHandler library, a SQL Script to create the single log table and two stored procedures, and a test Web Appliication with the handler set up so you can try the Test.aspx page and view the Report.aspx page to see the logged items.  It works well for me and is extremely easy to set up. If it helps you in any way, I'm happy to make that contribution.


Here's the main ExceptionLogger class:

using System;
using System.Web;
using System.Diagnostics;
using System.Data;
using System.Data.SqlClient;
using System.Web.Mail;
using System.Text;
using System.Reflection;
using System.Configuration;
using System.Net.Mail;
namespace PAB.ExceptionHandler
{
public static class  ExceptionLogger  
{
private static bool logExceptions = Convert.ToBoolean(ConfigurationManager.AppSettings["logExceptions"]);

public static Guid HandleException( Exception ex)
{
             if (HttpContext.Current != null)
            {
                 if (HttpContext.Current.Request != null)
                 {
                     ex.Data.Add("IP", HttpContext.Current.Request.UserHostAddress.ToString());
                      if(HttpContext.Current.Request.UrlReferrer!=null)
                     ex.Data.Add("Referer", HttpContext.Current.Request.UrlReferrer.ToString());
                }

               if (ex.InnerException != null)
                 {
                     ex.Data.Add("Inner Exception", ex.InnerException.Message);
                     ex.Data.Add("Inner StackTrace",ex.InnerException.StackTrace );
                 }

            }
Guid retval = Guid.Empty;
            HttpContext ctx = null;
            try
            {
                ctx = HttpContext.Current;
            }
            catch { }

string strData=String.Empty;
Guid eventId = System.Guid.NewGuid();
           string dbConnString=ConfigurationManager.AppSettings["exceptionLogConnString"].ToString();
string referer=String.Empty;
         string sForm = String.Empty;
         try
         {
             if (ctx.Request.UrlReferrer  != null)
             {
                 referer = ctx.Request.UrlReferrer.ToString();
             }
        
  sForm =
(ctx.Request.Form !=null)?ctx.Request.Form.ToString():String.Empty;
         }
          catch { }
      
       string logDateTime =DateTime.Now.ToString();
       string app_name = string.Empty;
       string app_path = String.Empty;
       try
       {
           app_path = ctx.Request.RawUrl;
       }
       catch { }

       if (app_path != "")
       {
           if (app_path.IndexOf("\\", 1) > 0)
           {
               char[] strArray = app_path.ToCharArray();
               Array.Reverse(strArray);
               app_path = new string(strArray);
               app_path = app_path.Substring(1, app_path.IndexOf("\\", 1) - 1);
               strArray = app_path.ToCharArray();
               Array.Reverse(strArray);
               app_name = new string(strArray);
           }        
          
       }
       else { app_name = ""; }
       app_name = System.Environment.MachineName.ToString() + " / " + app_name;
       StringBuilder sb = new StringBuilder();
       foreach ( string key in ex.Data.Keys)
       {
           sb.Append(key);
           sb.Append("=");
           sb.Append(ex.Data[key].ToString());
           sb.Append("|");

       }
       if(sb.Length >0) sb.Remove(sb.Length-1, 1);
       string exData = sb.ToString();
       string sQuery = String.Empty;
       try
       {
           sQuery =
              (ctx.Request.QueryString != null) ? ctx.Request.QueryString.ToString() : String.Empty;
           strData = "\nSOURCE: " + ex.Source +
           "\nLogDateTime: " + logDateTime +
           "\nMESSAGE: " + ex.Message +
          "\nFORM: " + sForm +
          "\nQUERYSTRING: " + sQuery +
          "\nTARGETSITE: " + ex.TargetSite +
          "\nSTACKTRACE: " + ex.StackTrace +
          "\nData: " + exData +
          "\nAppName: " + app_name +
          "\nREFERER: " + referer;
       }
       catch { }

if(dbConnString.Length >0)
{                
SqlCommand cmd = new SqlCommand();
cmd.CommandType=CommandType.StoredProcedure;
cmd.CommandText="usp_WebAppLogsInsert";
SqlConnection cn = new SqlConnection(dbConnString);
cmd.Connection=cn;
cn.Open();
                /*
                @EventID UNIQUEIDENTIFIER,
                @AppName varchar(150),
                @source varchar(100),
                @LogDateTime dateTime,
                @Message varchar(1000),
                @Form varchar(4000),
                @QueryString varchar(2000),
                @TargetSite varchar(300),
                @StackTrace varchar(4000),
                @Referer varchar(250),
                @Data varchar(500),
                @Path varchar(300)
                 */

try
{
cmd.Parameters.Add(new SqlParameter("@EventId",eventId ));
                 cmd.Parameters.Add(new SqlParameter("@AppName", app_name));
                string source = ex.Source == null ? "" : ex.Source;                 

cmd.Parameters.Add(new SqlParameter("@source", source));
cmd.Parameters.Add(new SqlParameter("@LogDateTime", logDateTime));
cmd.Parameters.Add(new SqlParameter("@Message",ex.Message));
                 cmd.Parameters.Add(new SqlParameter("@Form",sForm));
cmd.Parameters.Add(new SqlParameter("@QueryString",sQuery));                
                string site = String.Empty;
                 try
                 {
                     if(ex.TargetSite !=null)
                    site = ex.TargetSite.ToString();
                }
                catch { }
cmd.Parameters.Add(new SqlParameter("@TargetSite", site));
                string stackTrace = String.Empty;
                 if (ex.StackTrace != null) stackTrace = ex.StackTrace;              

cmd.Parameters.Add(new SqlParameter("@StackTrace",stackTrace));
cmd.Parameters.Add(new SqlParameter("@Referer",referer));
                 cmd.Parameters.Add(new SqlParameter("@Data", exData));
                string path = app_path;
                 cmd.Parameters.Add(new SqlParameter("@Path", path));
Object o = cmd.ExecuteScalar();
                      try
                     {
                          if(o!=null)
                        retval = (Guid)o;
                    }
                    catch { }
    }
catch (Exception exc)
{
// database error, not much you can do here except for debugging
         System.Diagnostics.Debug.WriteLine(exc.Message);
}
finally
{
cmd.Dispose();
cn.Close();
}
}


string strEmails =ConfigurationManager.AppSettings["emailAddresses"].ToString();
if (strEmails.Length >0)
{
string[] emails = strEmails.Split(Convert.ToChar(";"));
                string newemails=String.Join(",",emails);
string subject = "Web application error on " +System.Environment.MachineName;
string detailURL=ConfigurationManager.AppSettings["detailURL"].ToString();
string fullMessage=strData + " " + detailURL +"?EvtId="+ eventId.ToString() ;
                System.Net.Mail.MailMessage msg = new System.Net.Mail.MailMessage();
                 msg.To.Add(newemails);                
msg.Body=fullMessage;                
msg.Subject =subject;
try
{
                    System.Net.Mail.SmtpClient client = new SmtpClient();                
                     client.Send(msg);
}
catch (Exception ex2 )
{
// nothing worthwhile to do here other than for debugging.
}
  }
  return retval ;
     } // end method HandleException
   }
}

And here is the ExceptionManager class I added to "act like" EntLib:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PAB.ExceptionHandler
{
  public static  class ExceptionManager
    {
       public static void Process(Action action, string policyName)
      {
          if (action == null) throw new ArgumentNullException("action");
          if (policyName == null) throw new ArgumentNullException("policyName");

          try
          {
              action();
          }
          catch (Exception e)
          {
               e.Data.Add("Policy", policyName);
               ExceptionLogger.HandleException(e);
           }
       }


      public static TResult Process<TResult>(Func<TResult> action, TResult defaultResult, string policyName)
      {
          TResult result = defaultResult ;
         try
         {
           result=  action();
         }
        catch (Exception e)
        {
             e.Data.Add("Policy",policyName);
             ExceptionLogger.HandleException(e);
             
         }
           return result;
      }

     public static TResult Process<TResult>(Func<TResult> action, string policyName)
        {
             return Process(action, default(TResult), policyName);
        }
    }
}

And that's the whole thing! Easy, simple. You can download the Visual Studio 2010 demo solution. Unzip it. Execute the Table.sql script against the Sql Server database you want to log exceptions in. It even logs the Messsage and StackTrace of any InnerExceptions, an important thing to do because many Framework exceptions hide the real detail in the InnerException if present.

Fix the connection string in appSettings to match. Enter in your email information in the other fields, and try it out. This should be easy to customize, so please feel free. In the Test.aspx page there are lines you can uncomment that will generate a divide by zero exception to see how exceptions get logged. Incidentally, you don't need to use this just to log thrown exceptions. You can  use it to to log any information by simply creating a new System.Exception object, populating the Message and any other properties, and calling the HandleException method of the library.

Remember: Less is More. Don't overengineer, unless there is an overriding and persuasive need to do so.

By Peter Bromberg   Popularity  (3422 Views)