Formatting C# Enum ToString with Ease
c# enum tostring: Learn how to control C# enum ToString output, handle undefined values, 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 runtime provides several formatting options that can change the output, handle numeric values, and even throw in unexpected cases. The c# enum tostring behavior is straightforward when the value matches a defined member, but subtle differences appear with flags, undefined values, and culture. This article explains the exact behavior, the formatting specifiers you can use, and the practical decisions you need to make when 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
If the numeric value does not match any defined member, ToString() returns that numeric value as a string. For instance, if you have (LogLevel)42, calling ToString() gives "42". This means you must check Enum.IsDefined first if you only want to output known names.
The default behavior is equivalent to using the "G" format specifier, which stands for "general". The ToString() method on an enum 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 a standard .NET format string. The following specifiers are supported:
| 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 (without leading 0x). | 02 |
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 minimum number of hex digits needed to represent the underlying type (or two digits for a default int).
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 the [Flags] attribute, the G specifier would still produce the same output for a combination that sums to a defined member, but the F specifier forces the comma-separated behavior regardless. If a flag value includes undefined bits, the G and F specifiers will output the numeric value as a fallback, which can be misleading. Always validate the value with Enum.IsDefined for the exact combination or use Enum.GetValues to check each bit, depending on your needs.
The F specifier is particularly risky when the enum has a None value of 0 and you pass a value like 0. It returns "None" rather than an empty string, which is a subtle behavior to be aware of when building human-readable output.
Performance and Allocation Considerations
Converting an enum to a string involves memory allocation and, for the G and F specifiers, a lookup against the internal name table. This is not a free operation if you are calling it in a tight loop or for every log entry under high throughput. The underlying mechanism uses Enum.GetName which performs a binary search over sorted names, so the cost is logarithmic relative to the number of members.
If you need to convert enums to strings very frequently and the set of values is fixed, you may precompute a dictionary or an array of names to avoid repeated lookup. For example:
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) };
But be careful: if you use Enum.ToString() inside a custom attribute-driven system, the overhead is usually negligible compared to other operations like I/O. Profile first before adding 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 a 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 requires boxing the value because its signature takes object, which can incur a small allocation when the enum is passed by value. In modern .NET, you can use the generic overload Enum.GetName<TEnum>(TEnum value) introduced in .NET 8, which avoids boxing.
Note on Nullable and Default Values
If the enum value is 0 and the enum does not define a member for 0, both ToString() and Enum.GetName return the numeric string "0". This is a common pitfall when an enum is initialized to its default value, which is 0. Always 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.
There is also the nameof operator, which 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 is even faster than any runtime conversion. 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 (e.g., long), the X specifier will output the appropriate number of hex digits. For example, a long enum will output 16 hex digits padded with leading zeros. This is consistent with the underlying type's size.
If you are using string.Format or interpolated strings, the formatting of an enum is controlled by the ToString(string format) method. Interpolated strings like $"{level}" call the parameterless ToString(), so you cannot use a format specifier directly in an interpolation hole. Instead, call level.ToString("D") or use the format string in string.Format.