4
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

Functional Realization

  • It's very valuable to get additional information like exception's properties
  • it is nice to be able to sort exceptions
  • The access / inherit modifier of the members in stacktrace have no additional value because they are available in source code. I would prefer the original format: ([namespace]).[type].[member] (e.g. Printery.Print(String baz)).

Code

  • I don't like the usage of dynamic for that kind of tasks. Alternative to an additional custom type, it would be possible to return the string in your case.
  • It is not required, that the methode GetPropertiesExcept and the class Node are generic.
\$\endgroup\$
1
\$\begingroup\$

I can't see the necessity of the type parameter TException as it all works on base Exception?

This has an unnecessary if statement as you have just instantiated the text object, so its length will probably always be 0:

 if (text.Length > 0) { text.AppendLine(); }

This statement is not so nice:

foreach (var stackTrace in ex.GetStackTrace() ?? System.Linq.Enumerable.Empty<dynamic>())

Instead I would make sure GetStackTrace() always returns something enumerable. And since it is always returning the same kind of objects, I would make it returning a defined class instead of the use of dynamic.

I don't understand why you don't use the fact that it's all about Exceptions. Instead of collecting the subtree of Exceptions you could do it recursively avoiding the use of GetInnerExceptions():

public static string ToPrettyString(this Exception exception, int indentWidth = 4)
{
  return string.Join(Environment.NewLine, exception.ToPrettyString(0, indentWidth));
}

public static string ToReversePrettyString(this Exception exception, int indentWidth = 4)
{
  return string.Join(Environment.NewLine, exception.ToPrettyString(0, indentWidth).Reverse());
}

public static IEnumerable<string> ToPrettyString(this Exception exception, int indent, int indentWidth)
{
  var builder = new StringBuilder();

  var makeIndent = new Func<int, string>((depth) => new string(' ', indentWidth * (depth + indent)));

  builder.AppendLine($"{makeIndent(1)}{exception.GetType().Name}: \"{exception.Message}\"");

  if (exception is AggregateException)
  {
    builder.AppendLine($"{makeIndent(2)}InnerExceptions: \"{(exception as AggregateException).InnerExceptions.Count}\"");
  }

  foreach (var property in exception.GetPropertiesExcept<Exception>())
  {
    builder.AppendLine($"{makeIndent(1)}{property.Name}: \"{property.Value}\"");
  }

  foreach (var property in exception.GetData())
  {
    builder.AppendLine($"{makeIndent(1)}Data[{property.Key}]: \"{property.Value}\"");
  }

  builder.AppendLine($"{makeIndent(2)}StackTrace:");

  foreach (var stackTrace in exception.GetStackTrace() ?? System.Linq.Enumerable.Empty<dynamic>())
  {
    builder.AppendLine($"{makeIndent(3)}{stackTrace.Caller} in \"{stackTrace.FileName}\" Ln {stackTrace.LineNumber}");
  }

  yield return builder.ToString();

  if (exception is AggregateException)
  {
    foreach (var subStrings in (exception as AggregateException).InnerExceptions.Select(ex => ex.ToPrettyString(indent + 1, indentWidth)))
    {
      foreach (var subString in subStrings)
        yield return subString;
    }
  }
  else if (exception.InnerException != null)
  {
    foreach (var subString in exception.InnerException.ToPrettyString(indent + 1, indentWidth))
    {
      yield return subString;
    }
  }
}

... 
the rest of the class
\$\endgroup\$
  • \$\begingroup\$ I agree with you on the GetStackTrace. Using a tree is important here because it allows me to display the excpetions in ascending and descending orders. The TException is silly indeed. I must have left it there from the old version. \$\endgroup\$ – t3chb0t Jan 29 '17 at 15:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.