Created
March 22, 2021 13:36
-
-
Save he-dev/ddecec5babba41ef4536a5757a82ba62 to your computer and use it in GitHub Desktop.
ExceptionPrettifier for prettier exception strings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void Main(params string[] args) | |
{ | |
try | |
{ | |
try | |
{ | |
Printery.Print("abc"); | |
} | |
catch (Exception ex) | |
{ | |
throw new InvalidOperationException("This was wrong.", ex); | |
} | |
} | |
catch (Exception ex) | |
{ | |
ex.ToString().Dump(); | |
Console.WriteLine(""); | |
ex.ToPrettyString(ExceptionOrder.Ascending).Dump(); | |
} | |
} | |
class Printery | |
{ | |
public static void PrintLine(string qux) | |
{ | |
try | |
{ | |
throw new KeyNotFoundException("Where's the key?"); | |
} | |
catch (Exception ex) | |
{ | |
throw; | |
} | |
} | |
internal static void Print(string baz) | |
{ | |
var exceptions = new List<Exception>(); | |
try | |
{ | |
var fnfe = new FileNotFoundException(message: "The file wast't there.", fileName: "missing.txt"); | |
fnfe.Data["Not printed"] = baz; | |
throw fnfe; | |
} | |
catch (Exception ex) { exceptions.Add(ex); } | |
try | |
{ | |
//throw new EntitySqlException("Bad entity!"); | |
throw new DivideByZeroException("Blub!"); | |
} | |
catch (Exception ex) { exceptions.Add(ex); } | |
exceptions.Add(new AggregateException(new ArgumentException(), new NotSupportedException(), new ReadOnlyException())); | |
throw new AggregateException(exceptions); | |
} | |
} | |
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, level)).AppendLine($"{new string('.', level + 1)}"); | |
//text.Append(indent(0, depth)).AppendLine($"{depth}"); | |
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; | |
} | |
} | |
public enum ExceptionOrder | |
{ | |
Ascending, | |
Descending | |
} | |
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; | |
} | |
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--; | |
} | |
} | |
} | |
} | |
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(); | |
} | |
} | |
} | |
internal static class Enumerable | |
{ | |
public static IEnumerable<TArg> Except<TArg, TProjection>( | |
this IEnumerable<TArg> first, | |
IEnumerable<TArg> second, | |
Func<TArg, TProjection> projection) | |
{ | |
var mec = new AutoEqualityComparer<TArg, TProjection>(projection); | |
return first.Except(second, mec); | |
} | |
} | |
internal class AutoEqualityComparer<TArg, TProjection> : EqualityComparer<TArg> | |
{ | |
private readonly Func<TArg, TProjection> _projection; | |
public AutoEqualityComparer(Func<TArg, TProjection> projection) | |
{ | |
_projection = projection; | |
} | |
public override bool Equals(TArg left, TArg right) | |
{ | |
if (left == null && right == null) | |
{ | |
return true; | |
} | |
if (left == null) | |
{ | |
return false; | |
} | |
if (right == null) | |
{ | |
return false; | |
} | |
var xData = _projection(left); | |
var yData = _projection(right); | |
return EqualityComparer<TProjection>.Default.Equals(xData, yData); | |
} | |
public override int GetHashCode(TArg obj) | |
{ | |
if (obj == null) | |
{ | |
return 0; | |
} | |
var objData = _projection(obj); | |
return EqualityComparer<TProjection>.Default.GetHashCode(objData); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment