1
\$\begingroup\$

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 string
  • GetExceptionInfos 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" };
}
\$\endgroup\$

1 Answer 1

8
\$\begingroup\$
private const string Space = " ";

There's little point in storing this in a variable: it's never going to change anyway and you're not clarifying any of the intent. I'm fully aware that is a space and { is a left curly bracket.


Using dynamic is a very, very expensive action. By defaulting to it everywhere instead of using an intermediate type you're incurring heavy costs on yourself.


.Append(exceptionInfo.ExceptionMessage ?? "<Message>")

I'm not sure if <Message> is a default I'd like to see. It makes it feel as if something wasn't parsed correctly -- perhaps you can just leave it empty or give it an actual description like "No message found"?


internal class ExceptionStringFormatter : IExceptionFormatter

It has no instance fields so you might as well make it a static class with static members. That way you don't have to create a new object every time you want to format an exception string.


exceptionString.ToString().Dump();
return string.Empty;

Testing remnant?


var exceptionInfo = new ExpandoObject() as dynamic;

Might as well just write

dynamic exceptionInfo = new ExpandoObject();

AsEnumerable(this Exception exception)

Seems like a questionable name for what it does. Perhaps something like GetInnerExceptions() is more appropriate considering that's what it is actually implemented as and not just a projection of one exception to a list of exceptions.


var methodInfo = method as MethodInfo;

Use a direct cast or perform a null check. Right now you're just using the result directly which could mean you get a NullReferenceException -- a ClassCastException would be much more helpful in identifying the issue.


\$\endgroup\$
1
  • \$\begingroup\$ Ooops, yes .Dump() shouldn't be there (copy&paste from linqpad). I picked dynamics because exceptions themselves are very time consuming and actually shouldn't occur very often so I just thought a little bit of dynamics won't hurt it any further. As to new ExpandoObject() as dynamic - I just like var on the left side :-) As far as the other suggestions are concerned I agree and will definitely correct them ;-] \$\endgroup\$
    – t3chb0t
    Commented Jun 19, 2016 at 10:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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