C# Enum: Complete Guide to Enumerations in .NET
If you have ever written code with magic numbers scattered everywhere -- if (status == 2) or if (type == 4) -- you already know the pain that C# enum solves. Enumerations give names to numeric constants, making code readable, type-safe, and self-documenting. This complete guide covers everything: declaration, underlying types, flags, string conversion, switch pattern matching, and when to reach for an enum over a constant.
What Is a C# Enum?
An enum (short for enumeration) is a value type that defines a set of named constants. Under the hood, each enum member is an integer -- but you write code using names, not numbers.
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
By default, the first member has value 0, and each subsequent member increments by one. Pending is 0, Processing is 1, Shipped is 2, and so on.
The underlying type is int by default, but you can declare enums with byte, short, long, or any other integral type. This matters most for memory-sensitive scenarios or when working with external protocols and flags -- discussed further below.
Enums are value types, which means they are stored inline wherever the containing value is stored -- on the stack for local variables, or on the heap when part of a class instance. They participate in value equality: two enum variables holding the same member are equal. They also support boxing, cast to and from their underlying integer type, and work naturally with reflection.
Declaring and Assigning Enum Values
You can assign explicit values when the default numbering does not fit your domain:
public enum HttpStatusCode
{
Ok = 200,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500
}
Explicit values are useful when:
- The values must match an external system (HTTP codes, database columns, wire protocols)
- You want readable output when the enum is stored as an integer
- You need to leave gaps for future additions without breaking existing stored data
You can also mix explicit and implicit values. After an explicit value, the compiler continues incrementing:
public enum Priority
{
Low = 10,
Medium, // 11
High, // 12
Critical = 100
}
Mixing explicit and implicit assignments can be confusing for readers -- if you use explicit values for some members, make it explicit for all of them. Consistency matters more than brevity here.
Enum Underlying Types
By default, C# enums use int as their underlying numeric type. You can change this by specifying the type after the enum name:
// Using byte -- each value fits in 0-255
public enum Direction : byte
{
North = 0,
East = 1,
South = 2,
West = 3
}
// Using long -- needed for very large flag combinations
public enum LargePermissions : long
{
None = 0L,
Read = 1L << 0,
Write = 1L << 1,
Execute = 1L << 2
}
For most domain enums, int is correct and requires no explicit declaration. Reach for byte or short only when you are genuinely constrained on memory or interface compatibility. Use long for flags that need more than 32 distinct bits.
When you serialize enums to a database as integers, choosing the underlying type also determines the column type. A byte enum stored as a TINYINT saves space but limits you to 256 values. Think ahead before constraining yourself.
Flags Enum: Combining Multiple Values
A standard enum represents one exclusive value. A [Flags] enum lets you combine multiple values using bitwise OR:
[Flags]
public enum FileAccess
{
None = 0,
Read = 1, // 0001
Write = 2, // 0010
Execute = 4, // 0100
ReadWrite = Read | Write // 0011
}
Check whether a flag is set using bitwise AND or HasFlag:
FileAccess access = FileAccess.Read | FileAccess.Execute;
// Bitwise AND check
bool canRead = (access & FileAccess.Read) != 0; // true
bool canWrite = (access & FileAccess.Write) != 0; // false
bool canExecute = (access & FileAccess.Execute) != 0; // true
// HasFlag is cleaner but slightly slower due to boxing in older runtimes
bool alsoCanRead = access.HasFlag(FileAccess.Read); // true
The [Flags] attribute changes how ToString() formats combined values. Without it, Read | Execute produces "5". With [Flags], it produces "Read, Execute". Always assign values as powers of two when using [Flags].
A common mistake is assigning values like 1, 2, 3, 4 to a flags enum. The value 3 collides with 1 | 2, making it impossible to distinguish "ReadWrite" from "Read and Write are both set." Always use powers of two, or define composite values explicitly as shown above.
For a deeper dive into bitwise patterns and real-world flags usage, see Enums in CSharp -- A Simple Guide To Expressive Code.
Enum to String Conversion and Parsing
Converting an enum to its name and parsing a string back are common operations in logging, serialization, and API communication:
OrderStatus status = OrderStatus.Shipped;
// Enum to string
string name = status.ToString(); // "Shipped"
string name2 = Enum.GetName(status); // "Shipped" (.NET 5+)
// String to enum (throws ArgumentException if name is invalid)
OrderStatus parsed = Enum.Parse<OrderStatus>("Delivered");
// Safe parse -- returns false instead of throwing
if (Enum.TryParse("Cancelled", out OrderStatus result))
{
Console.WriteLine(result); // Cancelled
}
// Case-insensitive parse
bool ok = Enum.TryParse("SHIPPED", ignoreCase: true, out OrderStatus ci);
Serialization is a frequent source of bugs. System.Text.Json serializes enums as integers by default unless a converter such as JsonStringEnumConverter is configured -- producing 2 in JSON instead of "Shipped". Add JsonStringEnumConverter to change this:
// Program.cs / Startup.cs
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(
new System.Text.Json.Serialization.JsonStringEnumConverter());
});
With this in place, your API receives and sends "Shipped" rather than 2, making debugging and client integration much easier.
Switch Expressions With C# Enum
Enums pair naturally with switch. The switch expression introduced in C# 8 makes the pattern concise and enables exhaustive arm coverage:
string Describe(OrderStatus status) => status switch
{
OrderStatus.Pending => "Waiting to be processed",
OrderStatus.Processing => "Currently being handled",
OrderStatus.Shipped => "On its way to you",
OrderStatus.Delivered => "Delivered successfully",
OrderStatus.Cancelled => "Order was cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status))
};
The discard arm _ throws for unrecognized values -- important for defense against future members or unexpected integer casts into the enum. If you add a new member to OrderStatus, the _ arm catches it at runtime immediately.
A classic switch statement still works well when you need fallthrough behavior or side effects:
switch (status)
{
case OrderStatus.Shipped:
case OrderStatus.Delivered:
NotifyCustomer(order);
break;
case OrderStatus.Cancelled:
IssueRefund(order);
break;
default:
break;
}
For a complete walkthrough of switch patterns in C#, see Beginner's CSharp Switch Statement Tutorial and The CSharp Switch Statement -- How To Go From Zero To Hero.
Iterating Over Enum Members
Two common patterns for getting all defined members of an enum:
// .NET 5+ generic syntax
foreach (OrderStatus status in Enum.GetValues<OrderStatus>())
{
Console.WriteLine($"{status} = {(int)status}");
}
// Output:
// Pending = 0
// Processing = 1
// Shipped = 2
// Delivered = 3
// Cancelled = 4
// Get all names as strings
string[] names = Enum.GetNames<OrderStatus>();
// Legacy syntax for older runtimes
foreach (OrderStatus status in (OrderStatus[])Enum.GetValues(typeof(OrderStatus)))
{
// ...
}
Common uses for iteration: populating dropdowns, validating incoming integer values, generating lookup tables. Always validate that an integer from an external source represents a defined member:
public static bool IsValidEnumValue<TEnum>(int value) where TEnum : struct, Enum
=> Enum.IsDefined(typeof(TEnum), value);
// Protect against invalid data from external APIs or databases
if (!IsValidEnumValue<OrderStatus>(incomingStatusCode))
{
throw new ArgumentException($"Unrecognized status code: {incomingStatusCode}");
}
C# Enum Best Practices
Always define a zero-value member. An uninitialized enum field has value 0. If no member maps to 0, that variable is in an unnamed, undefined state -- which causes silent bugs in switch statements and serialization:
// Dangerous -- no zero value; default(Role) is unnamed
public enum Role
{
Admin = 1,
Editor = 2,
Viewer = 3
}
// Safe -- default state is explicitly named
public enum Role
{
None = 0,
Admin = 1,
Editor = 2,
Viewer = 3
}
Use singular names. Write OrderStatus, not OrderStatuses. Each value represents one status.
Do not use enums for open sets. If valid values change at runtime (user-defined categories, dynamically loaded configurations), use a string or value object instead.
Avoid casting to and from int in business logic. Explicit casts bypass type safety. If you need a numeric representation for external persistence, encapsulate that conversion in a dedicated repository or mapping layer.
Keep enums focused. An enum with 30 members is often doing too many jobs. Consider whether it should be split into smaller, focused enums.
For common mistakes to avoid, You're Using Enums Wrong walks through real anti-patterns and how to replace them.
Enum and the State Design Pattern
Enums work naturally with the State Design Pattern in C#. An OrderStatus enum maps directly to valid states, and a switch expression drives valid transitions:
public enum OrderEvent { StartProcessing, Ship, Deliver, Cancel }
public OrderStatus Transition(OrderStatus current, OrderEvent @event) =>
(current, @event) switch
{
(OrderStatus.Pending, OrderEvent.StartProcessing) => OrderStatus.Processing,
(OrderStatus.Processing, OrderEvent.Ship) => OrderStatus.Shipped,
(OrderStatus.Shipped, OrderEvent.Deliver) => OrderStatus.Delivered,
(OrderStatus.Pending, OrderEvent.Cancel) => OrderStatus.Cancelled,
(OrderStatus.Processing, OrderEvent.Cancel) => OrderStatus.Cancelled,
_ => throw new InvalidOperationException(
$"Transition {@event} is not valid from state {current}")
};
This pattern centralizes all valid transitions, makes illegal transitions explicit at runtime, and is easy to unit test. It is a natural bridge from a simple enum to a richer state machine as domain complexity grows. For the full pattern, see State Pattern Best Practices in C#.
Frequently Asked Questions
What is a C# enum?
A C# enum is a named set of integer constants grouped under a single type. It replaces magic numbers with meaningful names, ensures a variable can only hold valid values from the defined set, and enables exhaustive pattern matching in switch expressions.
What is the default value of a C# enum?
The default value is 0, regardless of what members are defined. Always define a named member for 0 (commonly None, Unknown, or a meaningful default) to prevent uninitialized variables from silently representing an undefined state.
How do I convert a C# enum to a string?
Call .ToString() on the enum value, or use Enum.GetName<T>(value) in .NET 5+. To serialize enum values as names in ASP.NET Core JSON responses, add JsonStringEnumConverter to your JSON options.
Can a C# enum hold multiple values at once?
Yes, using the [Flags] attribute with power-of-two assignments. Combine values with bitwise OR (|) and check individual flags with HasFlag() or bitwise AND (&).
How do I iterate over all members of an enum?
Use Enum.GetValues<T>() in .NET 5+ or (T[])Enum.GetValues(typeof(T)) in earlier versions. This returns all defined members in declaration order.
When should I use an enum instead of a constant?
Use an enum when a variable must hold one of a bounded, related set of choices -- order status, days of the week, log level. Use constants (const or static readonly) for standalone, unrelated numeric or string configuration values that don't belong to a group.
What is the [Flags] attribute on an enum?
[Flags] marks an enum for bitwise combination. It changes how ToString() renders combined values (showing names instead of a raw number) and signals to readers that multiple members can be combined. You must assign power-of-two values manually -- the attribute does not do that automatically.

