I find the default Exception.ToString()
formattig very difficult to read especially if there are inner exceptions involved. I guess everyone knows how it looks:
System.ArgumentException: This is a message. ---> UserQuery+TestException: You need to specify a valid id.
at UserQuery.foo(Int32 id) in C:\[...]\LINQPad5\_vheqmalf\query_bkavzy.cs:line 56
at UserQuery.Main() in C:\[...]\LINQPad5\_vheqmalf\query_bkavzy.cs:line 37
--- End of inner exception stack trace ---
at UserQuery.Main() in C:\[...]\LINQPad5\_vheqmalf\query_bkavzy.cs:line 41
So I thought I create my own formatting and later a base exception to get a better result - at least one that is more pleasing my eyes ;-)
TestException » You need to specify a valid id.
{
ExtraInfo:
[
Bar: "Baz"
]
StackTrace:
[
System.String UserQuery.foo(System.Int32 id) » C:\[...]\LINQPad5\_vheqmalf\query_bkavzy.cs:56
System.Void UserQuery.Main() » C:\[...]\LINQPad5\_vheqmalf\query_bkavzy.cs:37
]
}
ArgumentException » This is a message.
{
ExtraInfo:
[
ParamName: ""
]
StackTrace:
[
System.Void UserQuery.Main() » C:\[...]\LINQPad5\_vheqmalf\query_bkavzy.cs:41
]
}
It's similar to json but It shouldn't be json. It should be easy to read. I'm not going to parse it.
To keep the implementation short I chose to use mainly dynamic objects for the formatter. It has two methods:
Format
that takes an exception and turns it into as stringGetExceptionInfos
that extracts the relevant data from each exception
ExceptionStringFormatter
class:
internal class ExceptionStringFormatter : IExceptionFormatter
{
private const string Space = " ";
private const string CurlyBracketLeft = "{";
private const string CurlyBracketRight = "}";
private const string SquareBracketLeft = "[";
private const string SquareBracketRight = "]";
private const string RaquoSpacer = " » ";
private const int IndentWidth = 3;
public string Format<TException>(TException exception)
where TException : Exception
{
var exceptionInfos = GetExceptionInfos(exception).Reverse();
var exceptionString = new StringBuilder();
foreach (dynamic exceptionInfo in exceptionInfos)
{
exceptionString
.Append(exceptionInfo.ExceptionType.Name).Append(RaquoSpacer)
.Append(exceptionInfo.ExceptionMessage ?? "<Message>")
.AppendLine();
exceptionString.Append(CurlyBracketLeft).AppendLine();
// format properties
exceptionString.Append(Indent(1)).Append("ExtraInfo:").AppendLine();
exceptionString.Append(Indent(1)).Append(SquareBracketLeft).AppendLine();
foreach (var property in (IDictionary<string, object>)exceptionInfo.CustomProperties)
{
exceptionString
.Append(Indent(2))
.Append(property.Key + ": ")
.Append("\"" + property.Value + "\"")
.AppendLine();
}
exceptionString.Append(Indent(1)).Append(SquareBracketRight).AppendLine();
// format stack trace
exceptionString.Append(Indent(1)).Append("StackTrace:").AppendLine();
exceptionString.Append(Indent(1)).Append(SquareBracketLeft).AppendLine();
foreach (var stackFrame in exceptionInfo.StackTrace)
{
exceptionString
.Append(Indent(2))
.Append(stackFrame.CallerSignature).Append(RaquoSpacer)
.Append(stackFrame.FileName)
.Append(":" + stackFrame.LineNumber)
.AppendLine();
}
exceptionString.Append(Indent(1)).Append(SquareBracketRight).AppendLine();
exceptionString.Append(CurlyBracketRight).AppendLine();
}
exceptionString.ToString().Dump();
return string.Empty;
}
internal static IEnumerable<dynamic> GetExceptionInfos<TException>(TException exception)
where TException : Exception
{
foreach (var ex in exception.AsEnumerable())
{
var exceptionInfo = new ExpandoObject() as dynamic;
exceptionInfo.ExceptionType = ex.GetType();
exceptionInfo.ExceptionMessage = ex.Message;
exceptionInfo.CustomProperties = new ExpandoObject() as dynamic;
foreach (var property in ex.GetCustomProperties())
{
((IDictionary<string, object>)exceptionInfo.CustomProperties)[property.Key] = property.Value;
}
var strackTrace = new StackTrace(ex, true);
var stackFrames = strackTrace.GetFrames();
exceptionInfo.StackTrace = stackFrames?.Select(sf =>
{
var stackFrame = new ExpandoObject() as dynamic;
stackFrame.CallerSignature = sf.GetMethod().Format();
stackFrame.FileName = sf.GetFileName();
stackFrame.LineNumber = sf.GetFileLineNumber();
return stackFrame;
}).ToList();
yield return exceptionInfo;
}
}
private static string Indent(int depth)
{
return string.Empty.PadLeft(IndentWidth * depth);
}
}
There are also two helper classes.
One is the ExceptionExtensions
. Its purpose is to enumerate the inner exceptions and to get the custom properties from each one.
internal static class ExceptionExtensions
{
private static readonly HashSet<string> ExceptionPropertyNames = new HashSet<string>(
typeof(Exception)
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(p => p.Name));
public static IDictionary<string, object> GetCustomProperties<TException>(this TException exception)
where TException : Exception
{
var currentExceptionProperties =
exception.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
.Where(p => !ExceptionPropertyNames.Contains(p.Name));
var result = currentExceptionProperties.ToDictionary(property =>
property.Name,
property => property.GetValue(exception));
return result;
}
public static IEnumerable<Exception> AsEnumerable(this Exception exception)
{
var currentException = exception;
do
{
yield return currentException;
currentException = currentException.InnerException;
} while (currentException != null);
}
}
and the other one is the MethodSignatureFormatter
that creates method signature from method-info:
internal static class MethodSignatureFormatter
{
public static string Format(this MethodBase method)
{
var methodInfo = method as MethodInfo;
var signature =
new StringBuilder()
.Append(methodInfo.ReturnType.FullName)
.Append(" ").Append(methodInfo.DeclaringType.FullName)
.Append(".").Append(methodInfo.Name)
.Append("(")
.Append(string.Join(", ", methodInfo.GetParameters().Select(p =>
p.ParameterType.FullName + " " + p.Name)))
.Append(")");
return signature.ToString();
}
}
Base exception with custom formatting:
public abstract class FormattableException : Exception
{
protected FormattableException(string message, Exception innerException)
: base(message, innerException) { }
protected FormattableException(string message) : base(message) { }
public IExceptionFormatter Formatter { get; set; } = new ExceptionStringFormatter();
public override string ToString()
{
return Formatter?.Format(this) ?? base.ToString();
}
}
Demo:
void Main()
{
try
{
try
{
foo(9);
}
catch (Exception ex)
{
throw new ArgumentException("This is a message.", ex);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Console.WriteLine("-");
Console.WriteLine(new ExceptionStringFormatter().Format(ex));
}
}
string foo(int id)
{
throw new TestException("You need to specify a valid id.") { Bar = "Baz" };
}