Formatting C# Enum ToString with Ease
Learn how to control C# enum ToString output, handle undefined values and flags, and choose efficient conversion strategies.
When you call ToString() on an enum value in C#, you might expect the name as declared in code, but the method supports format specifiers that can change the output, handle numeric values, and throw for unsupported formats. The behavior of Enum.ToString() is straightforward when the value matches a defined member, but subtle differences appear with [Flags] enums and undefined values. This article explains the default behavior, the supported format specifiers, and the practical choices for converting enums to strings in production code.
The Default ToString Behavior
By default, Enum.ToString() returns the string name of the enum member if the value exactly matches a defined member. For example:
public enum LogLevel { Debug, Info, Warning, Error } LogLevel level = LogLevel.Warning; Console.WriteLine(level.ToString()); // Output: Warning
For a plain (non-flags) enum, if the numeric value does not match any defined member, ToString() returns that numeric value as a string. For instance, ((LogLevel)42).ToString() gives "42". If you only want to output known names, check Enum.IsDefined first. A [Flags] enum can instead return comma-separated names for a valid bit combination, as shown later.
The default behavior is equivalent to using the "G" format specifier, which stands for "general". ToString() respects the [Flags] attribute when the value represents a combination of named flags.
Using Format Specifiers to Control Output
The Enum.ToString(string format) overload accepts the following format specifiers:
| Specifier | Description | Example Output for LogLevel.Warning |
|---|---|---|
G | If the value is defined, returns the name(s); otherwise the numeric string. | Warning |
F | Treats the value as a set of flags and outputs comma-separated names (even without [Flags]). | Warning |
D | Returns the decimal numeric value. | 2 |
X | Returns the hexadecimal value, padded to the width of the underlying type (8 digits for int). | 00000002 |
For a flags combination, the G specifier also outputs comma-separated names if the value matches a combination of named members. The X specifier outputs the underlying value in hexadecimal padded to the size of the underlying type: 8 digits for an int-backed enum and 16 digits for a long-backed enum.
Handling Flags Enums Correctly
The [Flags] attribute changes how ToString() interprets a value. Consider:
[Flags] public enum FileAccess { None = 0, Read = 1, Write = 2, Execute = 4 } FileAccess access = FileAccess.Read | FileAccess.Write; Console.WriteLine(access.ToString()); // Output: Read, Write
Without [Flags], the G specifier does not decompose a value into named bits; (FileAccess)3 would be returned as "3" unless the enum defines a member with that value. The F specifier forces the comma-separated flag behavior even when the enum lacks [Flags]. If a flag value includes undefined bits, the G and F specifiers fall back to the numeric value, which can be misleading. Validate the value with Enum.IsDefined for exact named combinations, or use Enum.GetValues to check individual bits, depending on your needs.
The F specifier is worth watching when the enum has a None value of 0 and you format 0: it returns "None" rather than an empty string. That is usually the desired human-readable output, but it is a behavior to account for when building strings.
Performance and Allocation Considerations
Converting an enum to a string allocates a string and, for the G and F specifiers, requires a lookup in the enum's name/value table. This is not free if you call it in a tight loop or for every log entry under high throughput. If you need to convert the same fixed set of values frequently, precompute a dictionary or an array of names.
private static readonly IReadOnlyDictionary<LogLevel, string> logNameMap = new Dictionary<LogLevel, string> { [LogLevel.Debug] = nameof(LogLevel.Debug), [LogLevel.Info] = nameof(LogLevel.Info), [LogLevel.Warning] = nameof(LogLevel.Warning), [LogLevel.Error] = nameof(LogLevel.Error) };
If you use Enum.ToString() in a custom attribute-driven system, the overhead is usually negligible compared with I/O or other operations. Profile before adding caching complexity.
Using Enum.GetName for More Control
The Enum.GetName(Type, object) static method returns the name of the constant that has the specified value, or null if no match is found. This is more explicit than calling ToString() because it gives you null rather than falling back to the numeric string.
LogLevel level = (LogLevel)99; string? name = Enum.GetName(typeof(LogLevel), level); Console.WriteLine(name ?? "unknown"); // Output: unknown
When you need to handle undefined values gracefully, this pattern is often preferable to checking Enum.IsDefined separately because it combines lookup and validation in one call. However, Enum.GetName(Type, object) requires boxing the value because its signature takes object, which can incur a small allocation when the enum is passed by value. In .NET 8 and later, the generic overload Enum.GetName<TEnum>(TEnum value) avoids that boxing.
Note on Default Values and Zero
If the enum value is 0 and no member is defined for 0, ToString() returns "0", while Enum.GetName returns null. This distinction is important when an enum is initialized to its default value, which is 0. Define a member for 0, often None, to avoid unexpected output.
Choosing Between ToString and GetName in Production
For most applications, Enum.ToString() is sufficient and clear. Use Enum.GetName when you need to distinguish between a missing name and a valid name, or when you want to avoid the [Flags] interpretation. In performance-critical paths, precompute a dictionary.
The nameof operator yields the compile-time constant name of an enum member. It is useful when you know the exact member at compile time and want a constant string without any runtime lookup. nameof(LogLevel.Warning) always returns "Warning" and has no runtime conversion cost. Use it in attributes or when constructing human-readable output where the enum member is a literal.
Common Pitfalls and Edge Cases
When the same numeric value maps to multiple names (possible with duplicate underlying values), ToString() returns one of them, but not necessarily the first in declaration order. Enum.GetName also returns only one name. Do not rely on which one is chosen if duplicates exist; instead, refactor to remove duplicates for predictable output.
For enums with underlying values larger than int (for example, long), the X specifier outputs the appropriate number of hex digits. A long-backed enum outputs 16 hex digits padded with leading zeros, consistent with the underlying type's size.
Interpolated strings and string.Format both respect the enum's IFormattable implementation. $"{level:D}" and string.Format("{0:D}", level) call the formatted ToString(string format) overload. An interpolation without a format component, such as $"{level}", calls the parameterless ToString() method. If you prefer an explicit call, level.ToString("D") is equivalent.