HotDog's Blog

Hotdog (Robert Verpalen) about C# and vb.net

This blog hosted by:
http://blogs.vbcity.com      
  Home :: Syndication  :: Login

DecJanuary 2006Feb
SMTWTFS
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

Articles

Archives

Topics

CONTACT

Fun but useful linkies

General

VS 2005

Wolfenstein ET

Thursday, January 05, 2006 #

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

 

Code CopyHideScrollFull
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 information

sb.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 ="&nbsp;";
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--
posted @ 7:29 AM