ASP.NET Exception Handler Redux
By Peter Bromberg
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.
Popularity (2011 Views)
 |
| Biography - Peter Bromberg |
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking, financial and telephony for over 20 years. Pete focuses exclusively on the .NET Platform, and currently develops SOA and other .NET applications for a Fortune 500 clientele. Peter enjoys producing digital photo collage with Maya,playing jazz flute, the beach, and fine wines. You can view Peter's UnBlog and IttyUrl sites.
|  |
|
|
Article Discussion: ASP.NET Exception Handler Redux
Rick replied
to Peter Bromberg at Sunday, May 01, 2011 9:03 PM
Thanks Peter! I was looking for an example on this subject.
Rick
mandeep janjua replied
to Peter Bromberg at Sunday, May 01, 2011 9:03 PM
Hi Peter,
The article looks nice. You are using static classes and methods, I think it will cause trouble in an asp.net website in case couple of back to back exceptions happen.
Thanks,
Mandeep.
mandeep janjua replied
to Peter Bromberg at Sunday, May 01, 2011 9:03 PM
You can also use ELMAH and it takes couple of minutes to configure and will do the same thing which you are trying to do.
Peter Bromberg replied
to mandeep janjua at Sunday, May 01, 2011 9:03 PM
if a static method operates only on params passed to it - and those params are not shared either, it is threadsafe.
Peter Bromberg replied
to mandeep janjua at Sunday, May 01, 2011 9:03 PM
I looked at ELMAH and found it to be overly complex for such a simple purpose. I"m sure ELMAH is good, but the whole idea of this exercise is simplicity.