I've been refining my previous idea for pretty exception strings and made the following changes:
- Exceptions can be ordered.
- Prints all properties including
Data
. - The stack trace formatting is now more eye friendly.
- Added indentation for inner exceptions.
Examples
Output for Exception.ToString()
does not contain any of the inner aggregated exceptions:
System.InvalidOperationException: This was wrong. ---> System.AggregateException: One or more errors occurred. ---> System.IO.FileNotFoundException: The file wasn't there.
at Printery.Print(String baz) in C:\[...]\LINQPad5\_ptnftoal\query_embrhm.cs:line 75
--- End of inner exception stack trace ---
at Printery.Print(String baz) in C:\[...]\LINQPad5\_ptnftoal\query_embrhm.cs:line 86
at UserQuery.Main(String[] args) in C:\[...]\LINQPad5\_ptnftoal\query_embrhm.cs:line 38
--- End of inner exception stack trace ---
at UserQuery.Main(String[] args) in C:\[...]\LINQPad5\_ptnftoal\query_embrhm.cs:line 42
Output for Exception.ToPrettyString(ExceptionOrder.Ascending)
prints all exceptions in the original order, nested and with all available information:
FileNotFoundException: "The file wasn't there."
FileName: "missing.txt"
Data[Not printed]: "abc"
StackTrace:
Printery { internal static void Print(string baz) { ... } } in "query_embrhm.cs" Ln 75
EntitySqlException: "Bad entity!"
StackTrace:
Printery { internal static void Print(string baz) { ... } } in "query_embrhm.cs" Ln 80
ArgumentException: "Value does not fall within the expected range."
StackTrace:
NotSupportedException: "Specified method is not supported."
StackTrace:
ReadOnlyException: "Column is marked read only."
StackTrace:
AggregateException: "One or more errors occurred."
InnerExceptions: "3"
StackTrace:
AggregateException: "One or more errors occurred."
InnerExceptions: "3"
StackTrace:
Printery { internal static void Print(string baz) { ... } } in "query_embrhm.cs" Ln 86
UserQuery { private void Main(string[] args) { ... } } in "query_embrhm.cs" Ln 38
InvalidOperationException: "This was wrong."
StackTrace:
UserQuery { private void Main(string[] args) { ... } } in "query_embrhm.cs" Ln 42
Core
The main class is the ExceptionPrettifier
that provides one extension method ToPrettyString
.
I split the old solution into smaller pieces. I use dynamic
for convenience. In relation to exceptions that are already a big performance hit, I don't think this will have any significant influence.
public static class ExceptionPrettifier
{
public static string ToPrettyString<TException>(this TException exception, ExceptionOrder order = ExceptionOrder.Ascending, int indentWidth = 4) where TException : Exception
{
var exceptionStrings = new List<StringBuilder>();
var exceptions = exception.GetInnerExceptions();
var indent = new Func<int, int, string>((depth, nestedDepth) => new string(' ', indentWidth * (depth + nestedDepth)));
foreach (var node in exceptions)
{
var ex = node.Value;
var text = new StringBuilder();
var depth = (int)node.Depth;
if (text.Length > 0) { text.AppendLine(); }
text.Append(indent(0, depth)).AppendLine($"{ex.GetType().Name}: \"{ex.Message}\"");
if (node.Value is AggregateException)
{
text.Append(indent(1, depth)).AppendLine($"InnerExceptions: \"{((AggregateException)ex).InnerExceptions.Count}\"");
}
foreach (var property in ex.GetPropertiesExcept<Exception>())
{
text.Append(indent(1, depth)).AppendLine($"{property.Name}: \"{property.Value}\"");
}
foreach (var property in node.Value.GetData())
{
text.Append(indent(1, depth)).AppendLine($"Data[{property.Key}]: \"{property.Value}\"");
}
text.Append(indent(1, depth)).AppendLine($"StackTrace:");
foreach (var stackTrace in ex.GetStackTrace() ?? System.Linq.Enumerable.Empty<dynamic>())
{
text.Append(indent(2, depth)).AppendLine($"{stackTrace.Caller} in \"{stackTrace.FileName}\" Ln {stackTrace.LineNumber}");
}
exceptionStrings.Add(text);
}
if (order == ExceptionOrder.Ascending) { exceptionStrings.Reverse(); }
return string.Join(Environment.NewLine, exceptionStrings);
}
private static IEnumerable<dynamic> GetPropertiesExcept<TExceptException>(this Exception exception) where TExceptException : Exception
{
var propertyFlags = BindingFlags.Instance | BindingFlags.Public;
var properties = exception.GetType()
.GetProperties(propertyFlags)
.Except(typeof(TExceptException).GetProperties(propertyFlags), x => x.Name)
.Select(p => new { p.Name, Value = p.GetValue(exception) })
.Where(p => p.Value != null && !string.IsNullOrEmpty(p.Value as string));
return properties;
}
private static IEnumerable<dynamic> GetData(this Exception exception)
{
foreach (var key in exception.Data.Keys)
{
yield return new { Key = key, Value = exception.Data[key] };
}
}
private static IEnumerable<dynamic> GetStackTrace(this Exception exception)
{
var stackTrace = new StackTrace(exception, true);
var stackFrames = stackTrace.GetFrames();
var result = stackFrames?.Select(sf => new
{
Caller = (sf.GetMethod() as MethodInfo)?.ToShortString() ?? string.Empty,
FileName = Path.GetFileName(sf.GetFileName()),
LineNumber = sf.GetFileLineNumber(),
});
return result;
}
}
Helpers
There is an extension that gets all inner exceptions:
public static class Enumerator
{
public static IEnumerable<Node<Exception>> GetInnerExceptions(this Exception exception, bool includeCurrent = true)
{
if (exception == null) { throw new ArgumentNullException(nameof(exception)); }
var exceptionStack = new Stack<Node<Exception>>();
var depth = 0;
if (includeCurrent)
{
exceptionStack.Push(new Node<Exception>(exception, depth));
}
while (exceptionStack.Any())
{
var current = exceptionStack.Pop();
yield return current;
if (current.Value is AggregateException)
{
depth++;
foreach (var innerException in ((AggregateException)current).InnerExceptions)
{
exceptionStack.Push(new Node<Exception>(innerException, depth + 1));
}
continue;
}
if (current.Value.InnerException != null)
{
depth++;
exceptionStack.Push(new Node<Exception>(current.Value.InnerException, depth));
depth--;
}
}
}
}
The reflection helper creates pretty type strings:
public static class Reflection
{
public static string ToShortString(this MethodInfo method)
{
if (method == null) { throw new ArgumentNullException(nameof(method)); }
var indentWidth = 4;
var indent = new Func<int, string>(depth => new string(' ', indentWidth * depth));
var parameters = method.GetParameters().Select(p => $"{p.ParameterType.ToShortString()} {p.Name}");
// public/internal/protected/private [static] [abstract/virtual/override] retVal
var accessModifier = new[]
{
method.IsPublic ? "public" : string.Empty,
method.IsAssembly ? "internal" : string.Empty,
method.IsPrivate ? "private" : string.Empty,
method.IsFamily ? "protected" : string.Empty,
}
.First(x => !string.IsNullOrEmpty(x));
var inheritanceModifier = new[]
{
method.IsAbstract ? " abstract" : string.Empty,
method.IsVirtual ? " virtual" : string.Empty,
method.GetBaseDefinition() != method ? " override" : string.Empty,
}
.FirstOrDefault(x => !string.IsNullOrEmpty(x));
var signature = new StringBuilder()
.Append(method.DeclaringType?.FullName)
.Append(" { ")
.Append(accessModifier)
.Append(method.IsStatic ? " static" : string.Empty)
.Append(inheritanceModifier)
.Append(method.GetCustomAttribute<AsyncStateMachineAttribute>() != null ? " async" : string.Empty)
.Append(" ").Append(method.ReturnType.ToShortString())
.Append(" ").Append(method.Name)
.Append("(").Append(string.Join(", ", parameters)).Append(") { ... }")
.Append(" } ")
.ToString();
return signature;
}
public static string ToShortString(this Type type)
{
var codeDomProvider = CodeDomProvider.CreateProvider("C#");
var typeReferenceExpression = new CodeTypeReferenceExpression(type);
using (var writer = new StringWriter())
{
codeDomProvider.GenerateCodeFromExpression(typeReferenceExpression, writer, new CodeGeneratorOptions());
return writer.GetStringBuilder().ToString();
}
}
}
The node is just a general purpose node (I didn't want to use a Tuple
);
public class Node<T>
{
public Node(T value, int depth)
{
Value = value;
Depth = depth;
}
public T Value { get; }
public int Depth { get; }
public static implicit operator T(Node<T> node) => node.Value;
}
and the last piece is a small helper enum:
public enum ExceptionOrder
{
Ascending,
Descending
}
It won't run by just copy/paste. You'll need the AutoEqualityComparer
from my yesterday's question.