When exceptions are generated, you have some great debugging abilities in .net, but sometimes you want to save or mail those exceptions. For example batch programs can mail you their exceptions. The default stacktrace doesn't really cut it for me. You have to find your way back through the system methods before getting to the interesting parts. Besides that, I want to be able to see all inner exceptions as well without further actions.
The ExceptionInfo class below can return Exception info in an organized matter. It uses the great options .net already has (such as the System.Diagnostics.StackTrace class) and adds some functionality such as obtaining the lines of code the exception occured on (provided the code is ran in debug mode and the source files are reachable for the application the class is used in).
It also is able to output an exception overview in HTML format (including inner exceptions) which is for myself the most important option.
To show (the non html) information to the user, I use a custom form, but that's a bit of own styling, so not included here.
Some methods still have to be added, such as defaults for mailing or saving to disk, but the HTML generating portion does work, so if you write the GetTotalMessage() output to a .htm file or set it as the body of an html email (can be done using the available .net classes) you can use the file. For some of the other options, this class is still Under Construction
namespace Exceptions
{
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Reflection;
using System.Data.SqlClient;
using System.Collections; //nog generic collection used to keep portability to .net 1.1 and lower
public
class ExceptionInfo{
///
<summary>
/// The original Exception where this info object is based on
/// </summary>
public readonly Exception Exception;
public ExceptionInfo(
Exception ex)
{
this.Exception = ex;
//get stack trace information
StackTrace st =
new StackTrace(Exception,
true);
frames =
new ErrorFrame[st.FrameCount];
for (
int i = 0; i < frames.Length; i++)
{
frames[i] =
new ErrorFrame(st.GetFrame(i));
if (usercodeindex == -1 && frames[i].IsUserCode)
usercodeindex = i;
}
//determine total exception count
while (ex !=
null)
{
exceptioncount++;
ex = ex.InnerException;
}
}
#region General properties
/// <summary>
/// The date/time of the exception. This is not the time the exception
/// was thrown, but rather when the ExceptionInfo object was created.
/// For most debugging purposes that difference in time does not matter, but
/// when the exact time is required, do not rely on this value!
/// </summary>
public readonly DateTime DateTime = DateTime.Now;
ErrorFrame[] frames;
/// <summary>
/// Gets the different StackFrames that led to this
/// </summary>
public ErrorFrame[] Frames
{
get { return frames; }
}
public
int FrameCount
{
get { return frames.Length; }
}
int usercodeindex = -1;
/// <summary>
/// Returns the index of the first frame that occured in user code (and thus
/// the first frame that is actually debuggable ;-) )
/// </summary>
public int UserFrameIndex
{
get { return usercodeindex; }
}
///
<summary>
/// Information about the last step before this error occured. The returned
/// object contains information about that step, such as the method that was running.
/// When running in debug, the pdb files also provide info on the original code filename and line number
/// </summary>
public ErrorFrame LastFrame
{
get
{
if (frames.Length > 0) return frames[0];
return null;
} }
///
<summary>
/// Where <see cref="LastFrame"/> returns the last step in general, this property
/// returns the last step in non-system code
/// <seealso cref="UserFrameIndex"/>
/// </summary>
public ErrorFrame LastUserFrame
{
get
{
if (usercodeindex == -1) return null;
return frames[usercodeindex];
} }
///
<summary>
/// The exception type
/// </summary>
public string Type
{
get { return Exception.GetType().Name; }
}
#endregion
#region Mail NB: framework specific
/// This mail section was created in .net 2.0, you might
/// want to remove this part or alter it if using earlier versions
///
<summary>
/// Sends an email to the specified address
/// </summary>
/// <param name="To"></param>
public void Send(
string To)
{
System.Net.Mail.MailMessage m = GetExceptionMail();
m.To.Add(To);
System.Net.Mail.SmtpClient client = new System.Net.Mail.SmtpClient("mail");
client.Send(m);
}
///
<summary>
/// Send the exception info (in html format) to the addressee.
/// NB, some presumtions were made when trying to create a quick and
/// dirty send method. Safest way it to use <see cref="FillMailMessage"/> or
/// <see cref="GetExceptionMail"/> and do the sending manually
/// </summary>
/// <param name="ex"></param>
/// <param name="To"></param>
public static void Send(
Exception ex,
string To)
{
GetInfo(ex).Send(To);
}
///
<summary>
/// Gets a mailmessage instance containing the Exception info.
/// The from address is tried to be set to a custom address. If this
/// fails (or if you want to set a custom one), you'll have to set the From
/// address manually.
/// </summary>
/// <returns></returns>
public static System.Net.Mail.
MailMessage GetExceptionMail(
Exception ex)
{
return GetInfo(ex).GetExceptionMail();
}
System.Net.Mail.MailMessage GetExceptionMail()
{
System.Net.Mail.
MailMessage m =
new System.Net.Mail.
MailMessage();
FillMailMessage(m);
try{
m.From = new System.Net.Mail.MailAddress("Exceptions@" + Environment.UserDomainName);
}
catch { }
return m;
}
///
<summary>
/// Sets the body of the message to hold the exeption info
/// </summary>
/// <param name="m"></param>
public void FillMailMessage(System.Net.Mail.
MailMessage m)
{
m.IsBodyHtml = true;
m.Body = GetTotalMessage();
}
#endregion
#region ExtraInfo NB
/// <summary>
/// This class can hold extra info for an <see cref="ExceptionInfo"/>
/// Its main purpose is to output this extra info to the html text
/// </summary>
public abstract class ExtraInfo{
/// <summary>
/// With this method, the extra info writes itself (in html format) to
/// the stringbuilder
/// </summary>
/// <param name="sb"></param>
public abstract void AppendHTML(StringBuilder sb, ExceptionInfo ei);
public
static implicit operator ExtraInfo(
string text)
{
return new TextInfo(text);
}
}
public
class ValuesInfo :
ExtraInfo{
ArrayList values =
new ArrayList();
class ValueEntry{
public string Name, Text;
}
public string Header;
public
void AddText(
string text)
{
AddValue(null, text);
}
public void AddValue(
string Name,
string Value)
{
ValueEntry v = new ValueEntry();
v.Name = Name;
v.Text = Value;
values.Add(v);
}
public
override void AppendHTML(
StringBuilder sb,
ExceptionInfo ei)
{
ei.openBlock(1, 1);
if (Header !=
null)
sb.Append("<b><i>").Append(Header).Append(":</b></i>");
if (values.Count > 0)
{
ei.openBlock(1, 1);
foreach (
ValueEntry ve
in values)
{
if (ve.Name ==
null)
sb.Append(ve.Text);
else
ei.appendInfo(ve.Name, ve.Text);
}
ei.closeBlock();}
ei.closeBlock();
}
}
///
<summary>
/// Adds plain text as extrainfo
/// </summary>
public class TextInfo:
ExtraInfo{
public
string Text;
public TextInfo(
string text) { Text = text; }
public override void AppendHTML(
StringBuilder sb,
ExceptionInfo ei)
{
sb.Append(Text);
}
}
#region SQL extra info
/// <summary>
/// Used to display the history of sql strings.
/// </summary>
public class SQlStringInfo : ExtraInfo
{
ArrayList list =
new ArrayList();
public void Add(
string sql)
{
list.Add(sql);
}
public
override void AppendHTML(
StringBuilder sb,
ExceptionInfo ei)
{
ei.openBlock(1);
sb.Append(
"<B>Recent sql strings:</B>");
ei.openBlock(
"border:'gray 1 solid';margin-left:15");
int i = 0;
foreach (
string sql
in list)
{
sb.Append(
"<SPAN><A href=\"javascript:\" onclick=\"window.clipboardData.setData('Text',this.nextSibling.innerText);alert('Code copied to clipboard');\"><B>--")
.Append(i++ + 1)
.Append("--</B></A><span style='margin-left:10'>")
.Append(sql)
.Append("</span></span><BR>");
}
ei.closeBlock();
ei.closeBlock();
}
}
///
<summary>
/// Add one or more sql strings to the list to be displayed in the recent
/// sql list.
/// This is usefull when you keep a list of recent executed sql strings somewhere
/// and want to include them in the exception output
/// </summary>
/// <param name="SQL"></param>
public void AddSQLString(
params string[] SQL)
{
if (SQL.Length == 0)
return;
if (sqls ==
null)
{
sqls = new SQlStringInfo();
AddExtraInfo(sqls);
}
foreach (
string sql
in SQL)
{
sqls.Add(sql);
}
}
SQlStringInfo sqls;
#endregion
///
<summary>
/// Add <see cref="ExtraInfo"/>. You can also use a string as parameter
/// </summary>
/// <param name="ei"></param>
public void AddExtraInfo(
ExtraInfo ei)
{
if (extrainfo == null) extrainfo = new ArrayList();
extrainfo.Add(ei);
}
ArrayList extrainfo;
///
<summary>
/// returns the amount of <see cref="ExtraInfo"/> objects added
/// </summary>
public int ExtraInfoCount
{
get
{
if (extrainfo == null) return 0;
return extrainfo.Count;
} }
#region HTML
const string ExtraInfoAnchor =
"ExtraInfo";
protected virtual void appendExtraInfo()
{
if (extrainfo!=
null)
{
appendBR();
appendHR();
openBlock(2);
sb.Append(
"<B><A name='").Append(ExtraInfoAnchor)
.Append("'>Extra Info</A></B><BR><HR>");
openBlock(
"margin-left:10;font-size:smaller");
foreach (
ExtraInfo ei
in extrainfo)
{
ei.AppendHTML(sb, this);
}
closeBlock();
closeBlock();
}
}
#endregion
#endregion
#region Static
/// <summary>
/// Rather than creating a new instance manually, use
/// this method to choose the proper exception object
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
public static ExceptionInfo GetInfo(
Exception ex)
{
if (ex == null) return null;
if (ex is SqlException) return new SqlExceptionInfo(ex as SqlException);
return new ExceptionInfo(ex);
}
public static implicit operator ExceptionInfo(Exception ex)
{
return GetInfo(ex);
}
#endregion
#region Inner Exceptions
int exceptioncount;
/// <summary>
/// returns the total number of exceptions (Main Exception + all inner exceptions)
/// </summary>
public int ExceptionCount
{
get { return exceptioncount; }
}
#endregion
#region html information
/// BEWARE: this code is manufactured to quickly create the html information
/// and is not nicely constructed for reusability.
/// A bit more friendly code for html is used in the AutoFormatter (http://blogs.vbcity.com/hotdog/archive/2005/12/30/5759.aspx) ,
/// but did not use that here to keep the code portable for fresh applications
/// <summary>
/// Gets information about this exception and all inner exceptions in HTML format
/// </summary>
/// <returns></returns>
public string GetTotalMessage()
{
return AppendMessage(new StringBuilder()).ToString();
}
///
<summary>
/// This stringbuilder is only used in the html functions
/// </summary>
protected StringBuilder sb;
protected void openBlock()
{
openBlock(null);
}
protected void openBlock(
int Border)
{
openBlock(Border, 0);
}
protected void openBlock(
int Border,
int Indent)
{
openBlock(
"Border='" + Border +
"px black solid'"
+ (Indent > 0 ? "margin-left=" + (Indent*20) : null)
);
}
protected void openBlock(
string Style)
{
openclose( false,Style);
}
protected void closeBlock()
{
openclose(true,null);
}
void openclose(
bool close,
string Style)
{
sb.Append(
"<");
if (close) sb.Append(
"/");
sb.Append(
"DIV");
if (Style !=
null)
sb.Append(" Style=\"").Append(Style).Append("\"");
sb.Append(
">");
}
public StringBuilder AppendMessage(
StringBuilder builder)
{
sb = builder;
openBlock(2);
appendTop();
appendInfo("Exception info created on",DateTime.ToString());
appendInfo("Application", AppDomain.CurrentDomain.FriendlyName);
openBlock("font-size:smaller;margin-left:20");
appendHeaderBottom();
closeBlock();
Exception ex = Exception;
int depth = 0;
while (append(ex,depth++)) { ex = ex.InnerException; }
appendExtraInfo();
appendBottom();
closeBlock();
sb = null;
return builder;
}
///
<summary>
/// Gives inheriting classes the possibility to append html text at the top
/// of the info block, inside the main border
/// </summary>
protected virtual void appendTop()
{
}
/// <summary>
/// Gives inheriting classes the possibility to append html inside the header
/// of the info block.
/// The base functionality adds links to the main and inner exceptions (if there are any)
/// </summary>
protected virtual void appendHeaderBottom()
{
if (exceptioncount > 1)
{
for (
int i = 0; i < exceptioncount; i++)
{
openHeaderLink(AnchorNamePrefix + i);
AppendExceptionTitle(i);
sb.Append("</A>");
}
}
if (extrainfo != null)
{
openHeaderLink(ExtraInfoAnchor);
sb.Append("Extra Info</A>");
} }
/// <summary>
/// Gives inheriting classes the possibility to append html inside the header
/// of the Exception info block.
/// </summary>
protected virtual void appendExceptionHeader()
{
}
///
<summary>
/// Used to add links in the header
/// </summary>
/// <param name="href"></param>
/// <param name="name"></param>
protected virtual void openHeaderLink(
string href)
{
sb.Append(
"<A style='margin-left:15' href='#").Append(href)
.Append("'>");
}
/// <summary>
/// Gives inheriting classes the possibility to append html text at the
/// end of the exception info, but before the main block is closed
/// </summary>
protected virtual void appendBottom()
{
}
/// <summary>
/// The prefix of the name that is added per depth so that code can
/// point directly to one of the inner exceptions
/// The Name for the first exception is ExDepth0 , the second ExDepth1 and so forth.
/// Pointing to it in a href is subseqeuntly done with href = "#ExDepthX" where X is the inner exception index
/// </summary>
public const string AnchorNamePrefix = "ExDepth";
void AppendExceptionTitle(
int depth)
{
if (depth == 0)
sb.Append("Main Exception");
else
sb.Append("Inner Exception [").Append(depth).Append("]");
}
bool append(
Exception ex,
int depth)
{
if (ex ==
null)
return false;
appendHR();
//add anchor informationsb.Append(
"<A NAME=\"").Append(AnchorNamePrefix).Append(depth)
.Append("\" style='font-size:smaller'>");
AppendExceptionTitle(depth);
sb.Append(
": </A><span style='background-color:#990000;color:white;font-weight:bolder;width:100%'>")
.Append(ex.Message)
.Append("</span>");
openBlock(1, 1);
if (depth > 0)
{
ExceptionInfo ei = GetInfo(ex);
ei.sb = sb;
ei.appendHeader();
}
else
appendHeader();
closeBlock();
return true; }
const
string
usercodecolor = "lightblue",
systemcodecolor = "beige";
void appendHeader()
{
openBlock(
"border:'1 green solid';" );
appendInfo(
"Type", Type);
if(usercodeindex>=0)
{
ErrorFrame ef = LastUserFrame;
appendInfo("Last user code" , ef.FullMethodSignature + " (Line " + ef.Line + " in '" + ef.FileName + "')");
}
appendExceptionHeader();
if (frames.Length > 0)
{
appendTitle(
"Stack");
sb.Append(
"<span style='margin-left:20;font-size:smaller'>Legend: ");
for (
int i = 0; i < 2; i++)
{
sb.Append(
"<span style=\"background-color:")
.Append(i == 0 ? systemcodecolor : usercodecolor)
.Append("border='1 black solid';margin-left:15\">")
.Append(i == 0 ? "No debug information" : "With debug information")
.Append("</span>");
}
sb.Append(
"</span>");
openBlock(
"margin-left:40;font-size:smaller");
foreach (
ErrorFrame ef
in frames)
{
appendStack(ef);
}
closeBlock();
}
else
sb.Append("--No StackTrace available--");
closeBlock();}
protected void appendTitle(
string Name)
{
sb.Append("<B>").Append(Name).Append("</B>: ");
}
protected void appendInfo(
string Name,
string Value)
{
appendTitle(Name);
sb.Append(
"<SPAN style='position:relative;left:20'>")
.Append(Value).Append("</SPAN><BR>");
}
protected void appendHR()
{
sb.Append("<HR>");
}
protected void appendBR()
{
sb.Append("<BR>");
}
void appendStack(
ErrorFrame ef)
{
openBlock(
"border:'1 solid black';background-color:"
+ (ef.IsUserCode ? usercodecolor : systemcodecolor )
);
sb.Append(
"<span style='font-size:larger;font-style:italic;color:olive'><U>");
appendInfo(
"Method", ef.MethodName);
sb.Append(
"</U></span>");
appendInfo(
"NameSpace", ef.NameSpace);
appendInfo(
"Full Signature", ef.MethodSignature);
if (ef.IsUserCode)
{
appendTitle(
"File");
sb.Append(
"<A href='")
.Append(ef.FileName).Append("'>").Append(ef.FileName).Append("</A>")
.Append("<BR>");
appendInfo(
"Line", ef.Line.ToString());
appendTitle(
"Code snippet");
openBlock(
"border='1 black dotted';margin-left=25");
ef.AppendCode(sb, CodeSnippetExtraLines);
closeBlock();
}
closeBlock();
}
/// <summary>
/// Only applies when getting html text. This is the number of lines on
/// each side of the offending line, that should be included in code snippets
/// </summary>
public int CodeSnippetExtraLines = 3;
#endregion
public
override string ToString()
{
return Exception.ToString();
}
#region IO
///
<summary>
/// Shows the exception in html format by outputting to a default file first
/// and then opening it with the default browser.
/// (a form could have been used to do this, but tried to keep this info
/// class usable for Console applications as well)
/// </summary>
public void ShowException()
{
ShowExceptionFile("default");
}
///
<summary>
/// Shows the exception in a browser, see <see cref="ShowException()"/> for more info
/// </summary>
/// <param name="ex"></param>
public static void ShowException(
Exception ex)
{
GetInfo(ex).ShowException();
}
///
<summary>
/// First outputs the file (see <see cref="CreateExceptionFile"/>), then opens
/// it with the default connected viewer.
/// NB: the file is NOT appended. Existing info will be overwritten
/// NB2: if no writing is necessary
/// </summary>
/// <param name="file"></param>
public void ShowExceptionFile(
string file)
{
ShowExceptionFile(file, false);
}
/// <summary>
/// Same as <see cref="ShowExceptionFile(string)"/>, but with the option
/// to choose whether to append or not
/// </summary>
/// <param name="file"></param>
/// <param name="append"></param>
public void ShowExceptionFile(
string file,
bool append)
{
CreateExceptionFile(file, append);
ShowFile(file);
}
///
<summary>
/// All this does is run the file (using System.Diagnostics.Process.Start)
/// If an exception file is to be shown, make sure it is created first, or
/// use <see cref="ShowExceptionFile"/> to create and show the file instead
/// of this method
/// </summary>
/// <param name="file"></param>
public void ShowFile(
string file)
{
CheckFile(ref file);
Process.Start(file);
}
///
<summary>
/// Appends complete directory information to a name (see code for details ;-) )
/// </summary>
/// <param name="file"></param>
public void CheckFile(
ref string file)
{
FileInfo fi =
new FileInfo(file);
if (fi.FullName.Length > file.Length)
{
//no directory provided -> use application directory
string dir =
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
+ "\\Exceptions"
+ "\\" + AppDomain.CurrentDomain.FriendlyName
+ "\\";
if (!
Directory.Exists(dir))
Directory.CreateDirectory(dir);
file = dir + file; }
if (fi.Extension.Length == 0) file += ".htm"; }
///
<summary>
/// Outputs the exception to the specified file. Make sure the extension can
/// be read by explorer. If no extension is provided, ".htm" is used
/// </summary>
/// <param name="file"></param>
/// <param name="append"></param>
public void CreateExceptionFile(
string file,
bool append)
{
CheckFile(
ref file);
using (
StreamWriter sw =
new
StreamWriter(file, append, System.Text.Encoding.ASCII))
sw.Write(GetTotalMessage());
}
#endregion
}
public
class SqlExceptionInfo :
ExceptionInfo{
public
new readonly SqlException Exception;
public SqlExceptionInfo(
SqlException ex):
base(ex)
{
Exception = ex;
}
#region HTML
const string AnchorSQL = "sqlinfo";
protected
override void appendExceptionHeader()
{
base.appendTop();
appendTitle(
"SQL Errors (" + Exception.Errors.Count +
")");
openBlock(
"border:'1 gray solid';font-size:0.6em;margin-left:40;margin-right:20");
for (
int i = 0; i < Exception.Errors.Count; i++)
{
appendSQLError(i);
}
closeBlock();
}
protected override void appendHeaderBottom()
{
base.appendHeaderBottom();
openHeaderLink(AnchorSQL);
sb.Append("SQL info").Append("</A>");
}
void appendSQLError(
int index)
{
if (index > 0) appendHR();
SqlError se = Exception.Errors[index];
appendInfo("SQL class (severity)", se.Class.ToString());
appendInfo("Server", se.Server);
appendInfo("T-sql line number:", se.LineNumber.ToString());
appendInfo("Message", se.Message);
appendInfo("Procedure", se.Procedure);
appendInfo("Source", se.Source);
appendInfo("Sql error number", se.Number.ToString());
}
#endregion
}
///
<summary>
/// Wrapper around a <see cref="StackFrame"/>. Not that much added functionality, but
/// some properties instead of methods to be able to use easy databinding
/// </summary>
public class ErrorFrame{
public
readonly StackFrame Base;
public ErrorFrame(
StackFrame frame)
{
Base = frame;
}
public
int Line
{
get { return Base.GetFileLineNumber(); }
}
public string FileName
{
get { return Base.GetFileName(); }
}
public
string MethodName
{
get { return Base.GetMethod().Name; }
}
public
string MethodSignature
{
get { return Base.GetMethod().ToString(); }
}
public
string FullMethodSignature
{
get { return NameSpace + " - " + MethodSignature; }
}
public
string NameSpace
{
get { return Base.GetMethod().ReflectedType.FullName; }
}
public
bool IsUserCode
{
get { return Line > 0; }
}
public
MethodBase GetMethod()
{
return Base.GetMethod();
}
public
string GetCode()
{
return GetCode(2);
}
/// <summary>
///
/// </summary>
/// <param name="Lines">indicates the amount of lines before and after the Errorline to show </param>
public string GetCode(int Lines)
{
if (!IsUserCode)
return "No code available for a system method";
string file = FileName;
if (file ==
null || !
File.Exists(file))
return "File not found!";
try
{
return AppendCode(new StringBuilder(),Lines).ToString();
}
catch (Exception ex)
{
return "Error obtaining code information: " + ex.Message;
}
}
internal StringBuilder AppendCode(
StringBuilder sb,
int Lines)
{
int line = Line, from = line - Lines, to = line + Lines, curline = 0; ;
int len = sb.Length;
const string space =
" ";
string tab =
null;
for (
int tabcount = 0; tabcount < 4; tabcount++)
{
tab += space;
}
StreamReader sr =
null;
try{
sr =
new StreamReader(FileName);
string l;
while ((l = sr.ReadLine()) !=
null)
{
if (++curline>=from )
{
if (curline > to) break;
if (line == curline) sb.Append("<B>");
sb.Append(l.Replace("\t", tab).Replace(" ", space));
if (line == curline) sb.Append("</B>");
sb.Append("<BR>");
} } }
catch (
Exception ex)
{
sb.Length = len;
sb.Append(
"Error obtaining code information: ")
.Append(ex.Message);
}
finally{
if (sr != null) sr.Close();
}
return sb; }
}
}
. . .
An example of the output: --LINK coming up--