5
\$\begingroup\$

For classifying error reports, I need to generate an ID from an exception. That ID should be

  • (more or less) unique
  • a short alpha numeric string
  • equal for equivalent exceptions (even on different systems)

The following code seems to do the job:

    public static string GetExceptionID(this Exception ex)
    {
        if (ex == null) return "0000";

        int hash = 31;
        var innerException = ex;
        while (innerException != null)
        {
            hash ^= GetHash(innerException.GetType().ToString());

            hash ^= innerException.StackTrace
                                  .Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
                                  // If *.pdb file is provided, the file and line no. (e.g. "... in C:\Path...") is part of the stacktrace.
                                  // The next line cutting off that part
                                  .Select(line => line.Substring(0, line.IndexOf(" in ") < 0 ? line.Length : line.IndexOf(" in ")))
                                  .Aggregate(31, (currentHash, val) => currentHash ^= val.GetHash());

            innerException = innerException.InnerException;
        }

        return Convert.ToBase64String(BitConverter.GetBytes(hash))
            .Replace('+', 'P')
            .Replace('-', 'M')
            .Replace("=", "")
            .ToUpper();
    }

    // https://en.wikipedia.org/wiki/Jenkins_hash_function
    private static int GetHash(this string str)
    {
        if (str == null) return 1;

        int hash = 31;
        foreach (char c in str)
        {
            unchecked
            {
                hash += c;
                hash += hash << 10;
                hash ^= hash >> 6;
            }
        }

        hash += hash << 3;
        hash ^= hash >> 11;
        hash += hash << 15;

        return hash;
    }

Do you see any problems with the code?

\$\endgroup\$
  • \$\begingroup\$ Some create a unique ID such as SQLexception has Number \$\endgroup\$ – paparazzo Dec 13 '16 at 15:13
  • \$\begingroup\$ @Paparazzi: Yes, that's right... however, I need a solution that works with all kinds of exceptions ;) \$\endgroup\$ – JanDotNet Dec 13 '16 at 19:33
4
\$\begingroup\$

Stack trace

I think you shouldn't work with the stack string but retrieve the stack and go through each frame yourself. I find it would make the analysis more reliable and you could get the information you need without extracting it from the string because you would have direct access to each part.

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(),
    });
}

This snippet comes from my Exception prettyfier.


Other suggestions

It would be a good idea to split this method into three or four methods.

  • the first one gets you the information you need from the stack - you could implement it either based on the string or on the raw stack trace
  • the second one would encode the data as Base64String
  • the third and last one would clean-up or encode the string generated by the second one
  • the fourth could be a helper encapsulating the three other methods

Example:

var ex.ToExceptionId()

where

public static string ToExceptionId(this Exception ex)
{
    return ex.GetStackTrace().ToBase64String().EncodeBase64String();
}

Now you can test and maintain each part of the exception id creation separately.

\$\endgroup\$
  • 1
    \$\begingroup\$ Personally, I would never use dynamic just to avoid creating a real type as you do. It defeats the purpose of a statically typed language for no real gain. Also, you use the null coalescing operator on stackFrames, storing it in result (which you presumably had intended to actually return) -- that means if GetFrames() returns null, you'll return a null enumerable, which is bad form. But totally agree with generating the stack trace yourself -- came as a surprise to me to see in BugSnag that stack traces are localized! That "at" in front of each line will be in the user's language! \$\endgroup\$ – Kirk Woll Dec 13 '16 at 14:21
  • \$\begingroup\$ @KirkWoll your're right about the null enumerable, I should add a ?? Enumerable.Empty<T>() there. You can review my original question where I use it if you like :-) \$\endgroup\$ – t3chb0t Dec 13 '16 at 14:31
  • \$\begingroup\$ Thanks for you answer. It is a good idea to use the StackTrace class instead of parsing the stack trace as string! I've changed my implementation according to your suggestion ;) \$\endgroup\$ – JanDotNet Dec 13 '16 at 19:20
  • \$\begingroup\$ It turns out, that the solution with StackTrace does not work in my case, because the exceptions, I am working with, may be serialized/deserialized (BinaryFormatter). The serialization process drop theTargetSite property from the Exception object. Supposably, that property is requried for the StackTrace class to... \$\endgroup\$ – JanDotNet Dec 14 '16 at 14:07
  • \$\begingroup\$ @JanDotNet that's interesting. Can you create a simple example to reproduce this and post a link to pastebin or something? I was always wondering how the TargetSite works. This sounds like a good opportunity and maybe I can use it to improve my formatter :-D \$\endgroup\$ – t3chb0t Dec 14 '16 at 14:32

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.