Input Parsing to Known Value07/26/2026 | 5 minutes to read
A common task for Line of Business (LOB) applications is parsing an input string into an internal identifier. This blog post uses .NET 10 code samples. For simplicity, let's assume the inputs are non-malicious in terms of length, values, culture, etc.
Suppose you have a string input that needs to be parsed into one of the following enums:
public enum AccessLevel { NONE, READONLY, READWRITE, CREATE, DELETE, MANAGE_USER, CONFIGURE_SYSTEM, FULL_CONTROL }
I chose the enum names to be in uppercase ("shouting"). While this casing is not typical for C#, many applications define domain-specific terms in uppercase to match business terminology. If business analysts use uppercase names, the enums often follow suit.
Many developers prefer to create "enum-like classes" or records to hold an internal value and a string identifier that matches business needs. This approach allows the codebase to use more natural casing for static instances, independent of business terminology.
Enums often evolve over time, and their casing may change. For example, a database might store lowercased values, another system might use PascalCase, or a web frontend might pass camelCased values. How can an application parse string inputs into such enums (or enum-like classes)?
Enum.TryParse
The most common approach to parse string values into enums is the static TryParse method of the Enum type. This method has an overload that allows ignoring case. For example:
public AccessLevel EnumTryParse(string input) { Enum.TryParse<AccessLevel>(input, ignoreCase: true, out var result); return result; }
C# enums can hold multiple values as flags. The TryParse and Parse methods can parse a string representation of flags. However, most enums I work with are mutually exclusive. One downside of the TryParse method is that it is generally slower compared to direct string comparisons as it handles such general cases as well.
ToUpper
Some projects create extension methods or custom static TryParse methods to address mutual exclusivity and improve performance over Enum.TryParse. These methods are typically faster, but when case insensitivity is required, they often use ToUpperInvariant convert the input string's casing:
public AccessLevel ToUpper(string input) { var value = input.ToUpperInvariant(); return value switch { nameof(AccessLevel.READONLY) => AccessLevel.READONLY, nameof(AccessLevel.READWRITE) => AccessLevel.READWRITE, nameof(AccessLevel.CREATE) => AccessLevel.CREATE, nameof(AccessLevel.DELETE) => AccessLevel.DELETE, nameof(AccessLevel.MANAGE_USER) => AccessLevel.MANAGE_USER, nameof(AccessLevel.CONFIGURE_SYSTEM) => AccessLevel.CONFIGURE_SYSTEM, nameof(AccessLevel.FULL_CONTROL) => AccessLevel.FULL_CONTROL, _ => AccessLevel.NONE }; }
The ToUpperInvariant method allocates a new string on the heap. While .NET 10 and object stack allocation might optimize this, as shown later in the performance comparison, it still incurs overhead. I explored object-stack-allocation in a previous post.
CompareIgnoreCase
One way to avoid the heap allocation is to perform string equality tests with passing the OrdinalIgnoreCase flag.
public AccessLevel CompareIgnoreCase(string input) { if (string.Equals(input, nameof(AccessLevel.READONLY), StringComparison.OrdinalIgnoreCase)) return AccessLevel.READONLY; else if (string.Equals(input, nameof(AccessLevel.READWRITE), StringComparison.OrdinalIgnoreCase)) return AccessLevel.READWRITE; else if (string.Equals(input, nameof(AccessLevel.CREATE), StringComparison.OrdinalIgnoreCase)) return AccessLevel.CREATE; else if (string.Equals(input, nameof(AccessLevel.DELETE), StringComparison.OrdinalIgnoreCase)) return AccessLevel.DELETE; else if (string.Equals(input, nameof(AccessLevel.MANAGE_USER), StringComparison.OrdinalIgnoreCase)) return AccessLevel.MANAGE_USER; else if (string.Equals(input, nameof(AccessLevel.CONFIGURE_SYSTEM), StringComparison.OrdinalIgnoreCase)) return AccessLevel.CONFIGURE_SYSTEM; else if (string.Equals(input, nameof(AccessLevel.FULL_CONTROL), StringComparison.OrdinalIgnoreCase)) return AccessLevel.FULL_CONTROL; return AccessLevel.NONE; }
While this is fast, for enums with many values there might be a trade-off against the switch expressions used in the previous case. Switch expression can generate a lookup that can be faster to comparing each and every possible value.
ToUpperSpan
A hybrid approach combines the above solutions by using a stack-allocated buffer. This avoids heap allocation while enabling case-insensitive comparisons with switch expressions:
public AccessLevel ToUpperSpan(string input) { Span<char> value = stackalloc char[input.Length]; // Validate length and character classes for safety input.AsSpan().ToUpperInvariant(value); return value switch { nameof(AccessLevel.READONLY) => AccessLevel.READONLY, nameof(AccessLevel.READWRITE) => AccessLevel.READWRITE, nameof(AccessLevel.CREATE) => AccessLevel.CREATE, nameof(AccessLevel.DELETE) => AccessLevel.DELETE, nameof(AccessLevel.MANAGE_USER) => AccessLevel.MANAGE_USER, nameof(AccessLevel.CONFIGURE_SYSTEM) => AccessLevel.CONFIGURE_SYSTEM, nameof(AccessLevel.FULL_CONTROL) => AccessLevel.FULL_CONTROL, _ => AccessLevel.NONE }; }
Comparison
I used BenchmarkDotNet to compare the methods. Two input strings were tested: full_control and readonly. The results show that for enums with eight values the CompareIgnoreCase method is the fastest. The benchmarks also reveal that the ToUpper method allocates memory on the heap, while the other methods avoid this overhead.
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8039/25H2/2025Update/HudsonValley2) 12th Gen Intel Core i7-1255U 2.60GHz, 1 CPU, 12 logical and 10 physical cores .NET SDK 11.0.100-preview.2.26159.112 [Host] : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3 Method | Input | Mean | Error | StdDev | Gen0 | Allocated | |------------------ |------------- |-----------:|----------:|----------:|-------:|----------:| | CompareIgnoreCase | full_control | 2.7954 ns | 0.0384 ns | 0.0359 ns | - | - | | ToUpper | full_control | 16.2234 ns | 0.2050 ns | 0.1917 ns | 0.0016 | 48 B | | ToUpperSpan | full_control | 8.7174 ns | 0.0761 ns | 0.0675 ns | - | - | | EnumTryParse | full_control | 40.6552 ns | 0.3313 ns | 0.3099 ns | - | - | | CompareIgnoreCase | readonly | 0.2521 ns | 0.0134 ns | 0.0125 ns | - | - | | ToUpper | readonly | 12.1622 ns | 0.1104 ns | 0.1033 ns | 0.0020 | 40 B | | ToUpperSpan | readonly | 4.6476 ns | 0.0457 ns | 0.0427 ns | - | - | | EnumTryParse | readonly | 17.0223 ns | 0.1718 ns | 0.1607 ns | - | - |