Introduction

Enum type has few helper methods which make parsing strings into related enum value easy. There is Parse, as well as TryParse. Both can takes an extra parameter to specify whether the operation is case sensitive.

Unfortunately, it is not enough, as both methods will happily parse any natural number which fits underlying type (int by default):

   enum Colour { Red = 1, Green = 2, Blue = 3 }

   var s = Colour.TryParse("4", out Colour c)

   // s = true, c = 4

To determine whether given value exists in specified enumeration you can use Enum.IsDefined method:

   var a = Enum.IsDefined(typeof(Colour), c)
   // a = false

Performance considerations

Turns out above methods, as useful as they are, result in very poor performance. For majority of cases (see note at the end of this article) the performance would be acceptable, but in high-performance code this shows up as a bottleneck.

Fortunately, this can be easily solved through use of dictionary as a map between text and enum value:

   enum Colour { Red = 1, Green = 2, Blue = 3 }

    var d = new Dictionary<string, Colour>
        {
            { "Red", Colour.Red},
            { "1", Colour.Red},
            { "Green", Colour.Green},
            { "2", Colour.Green},
            { "Blue", Colour.Blue},
            { "3", Colour.Blue}
        };
   var s =  d.TryGetValue("4", out var c)

    // s = false, c = 0;

That will perform much better but there are two potential issues:

  • code repetition - values are defined twice: first in enum definition and then when filling up dictionary
  • case-sensitivity - current solution doesn't cover case-insensitive parsing.

Code repetition

Although it may not seem like much of a problem now, it could lead to a bug when we add new item to the Colour enum and forget to add it into related dictionary. By using Enum.GetValues() method we can automatically fill dictionary with relevant values:

    Enum
    .GetValues(typeof(Colour))
    .Cast<Colour>()
    .SelectMany(v =>
        new[] {
            (n: v.ToString().ToLower(), v), // get string representation of value
            (n: ((int)v).ToString(), v)     // get int representation of value
    })
    .ToDictionary(i => i.n, i => i.v);

Case insensitive parsing

If required, case-insensitive parsing may be done by providing relevant IEqualityComparer<TKey> to the constructor of the dictionary:

    Enum
    .GetValues(typeof(Colour))
    .Cast<Colour>()
    .SelectMany(v =>
        new[] {
                        (n: v.ToString().ToLower(), v), // get string representation of value
                        (n: ((int)v).ToString(), v)     // get int representation of value
    })
    .ToDictionary(i => i.n, i => i.v, StringComparer.OrdinalIgnoreCase);

Benchmarking

To verify impact of changes on performance I am using BenchmarkDotNet which is great and easy to use library for benchmarking .NET code.

I set up three test cases:

  • Using TryParse and IsDefined
  • Using TryParse only - good in cases where you can be sure values to parse are valid
  • Using mapping Dictionary discussed above

Running it on my laptop with Intel i7 CPU I got following results:

Method Mean Error StdDev
TryParseAndIsDefined 549.42 ms 6.479 ms 6.060 ms
TryParseOnly 313.08 ms 4.472 ms 3.965 ms
UsingDictionary 71.85 ms 1.331 ms 1.245 ms

As you can see, optimised version with mapping dictionary is over 7 time faster then TryParse with IsDefined with much lower error level!

Having that code in a tight-loop which is executed thousands of times a minute can significantly improve performance.

You can find Benchmark code here: https://github.com/mariuszwojcik/Blog-posts-code/blob/master/Benchmarks/EnumParse.cs

Note on performance optimisations

Performance optimisations can lead to code which is less readable and harder to maintain. It's always a good idea to profile the application to see whether optimisation is required. BenchmarkDotNet is a great tool to measure different approaches to solve the problem and how they improve speed and memory management.