Creating Named Constants with Enumerations (ENUM) in .NET

Introduction

Have you ever seen code like this scattered across a project?

if (userStatus == 1)
{
    // Active user
}
else if (userStatus == 2)
{
    // Suspended
}
else if (userStatus == 3)
{
    // Deleted
}

What does "1" mean? What about "2"? You need to hunt through documentation or read comments to understand the intent. Even worse, nothing stops you from accidentally setting userStatus = 99, creating a bug that compiles just fine.

Enumerations (enums) solve this problem by replacing magic numbers with named constants. Instead of cryptic integers, you get self-documenting code with compile-time type safety. An enum turns userStatus == 1 into userStatus == UserStatus.Active, making your code readable and preventing invalid values.

In this article, you'll learn how to create and use enums effectively in .NET. We'll cover basic enum syntax, the Flags attribute for combining values, conversion between enums and strings, and common pitfalls around default values and underlying types.

What Are Enums?

An enum is a value type that defines a set of named constants. Under the hood, each constant is stored as an integer (by default), but you work with meaningful names instead of raw numbers.

Here's a basic enum declaration:

public enum OrderStatus
{
    Pending,
    Processing,
    Shipped,
    Delivered,
    Cancelled
}

The compiler automatically assigns integer values starting from 0: Pending = 0, Processing = 1, and so on. You can also assign explicit values:

public enum OrderStatus
{
    Pending = 1,
    Processing = 2,
    Shipped = 3,
    Delivered = 4,
    Cancelled = 0
}

Notice how Cancelled = 0 gives you a meaningful zero value instead of the default. This prevents confusion when an uninitialized enum field defaults to 0.

Basic Enum Usage

Using enums is straightforward. You declare variables with the enum type and assign named values:

OrderStatus currentStatus = OrderStatus.Pending;

if (currentStatus == OrderStatus.Shipped)
{
    Console.WriteLine("Your order is on the way!");
}

The compiler enforces type safety. You can't accidentally assign a random integer or a value from a different enum:

// Compile error: cannot convert int to OrderStatus
// currentStatus = 42;

// Compile error: cannot convert UserStatus to OrderStatus
// currentStatus = UserStatus.Active;

This type safety is the main advantage over integer constants. With constants, any integer is valid. With enums, only the defined values are allowed (without explicit casting).

Switch Expressions with Enums

Enums work beautifully with switch expressions in modern C#:

string GetStatusMessage(OrderStatus status) => status switch
{
    OrderStatus.Pending => "We've received your order",
    OrderStatus.Processing => "We're preparing your order",
    OrderStatus.Shipped => "Your order is on the way",
    OrderStatus.Delivered => "Order delivered successfully",
    OrderStatus.Cancelled => "Order has been cancelled",
    _ => throw new ArgumentOutOfRangeException(nameof(status))
};

The compiler warns you if you forget to handle an enum value. This exhaustiveness checking helps catch bugs during development instead of at runtime.

Flags Enums for Combinations

Sometimes you need to combine multiple enum values. For example, file permissions might include Read, Write, and Execute all at once. The [Flags] attribute enables this:

[Flags]
public enum FilePermissions
{
    None = 0,
    Read = 1,
    Write = 2,
    Execute = 4,
    ReadWrite = Read | Write,
    All = Read | Write | Execute
}

Notice the powers of two (1, 2, 4). This lets you combine values using bitwise OR:

FilePermissions permissions = FilePermissions.Read | FilePermissions.Write;

if (permissions.HasFlag(FilePermissions.Read))
{
    Console.WriteLine("Read access granted");
}

if (permissions.HasFlag(FilePermissions.Execute))
{
    Console.WriteLine("This won't print");
}

The HasFlag method checks whether a specific flag is set. You can also use bitwise AND (&) for manual checking:

bool canWrite = (permissions & FilePermissions.Write) == FilePermissions.Write;

Try It Yourself

Create a new console project and experiment with enum conversions and validation. This example shows how to convert between enums, integers, and strings:

Program.cs:

var priority = Priority.Medium;

// Enum to int
int priorityValue = (int)priority;
Console.WriteLine($"Medium priority value: {priorityValue}");

// Int to enum (with validation)
int userInput = 5;
if (Enum.IsDefined(typeof(Priority), userInput))
{
    var convertedPriority = (Priority)userInput;
    Console.WriteLine($"Valid priority: {convertedPriority}");
}
else
{
    Console.WriteLine("Invalid priority value!");
}

// String to enum (safe parsing)
string statusText = "Urgent";
if (Enum.TryParse(statusText, out var parsedPriority))
{
    Console.WriteLine($"Parsed priority: {parsedPriority}");
}
else
{
    Console.WriteLine("Could not parse priority");
}

// Enum to string
Console.WriteLine($"Current priority: {priority}");

// Get all enum values
Console.WriteLine("\nAll priorities:");
foreach (Priority p in Enum.GetValues())
{
    Console.WriteLine($"- {p} ({(int)p})");
}

public enum Priority
{
    Low = 1,
    Medium = 3,
    High = 5,
    Urgent = 10
}

Project file (EnumConversion.csproj):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Expected output:

Medium priority value: 3
Invalid priority value!
Parsed priority: Urgent
Current priority: Medium

All priorities:
- Low (1)
- Medium (3)
- High (5)
- Urgent (10)

The example demonstrates safe conversion patterns. Notice how Enum.IsDefined validates integer values before casting, and Enum.TryParse handles string parsing without throwing exceptions.

Common Pitfalls to Avoid

Enums seem simple, but there are several gotchas that catch developers by surprise.

Undefined Zero Values

The biggest mistake is forgetting that the default enum value is always 0:

public enum PaymentMethod
{
    CreditCard = 1,
    DebitCard = 2,
    PayPal = 3
}

// This creates a PaymentMethod with value 0, which isn't defined!
PaymentMethod method = new PaymentMethod();
Console.WriteLine(method); // Prints: 0

Always define a zero value explicitly:

public enum PaymentMethod
{
    None = 0,  // or Unknown, NotSpecified, etc.
    CreditCard = 1,
    DebitCard = 2,
    PayPal = 3
}

Casting Without Validation

You can cast any integer to an enum type, even if the value isn't defined:

var invalidStatus = (OrderStatus)999;  // Compiles fine!
Console.WriteLine(invalidStatus);      // Prints: 999

Always validate when converting from user input or external data:

int input = GetUserInput();
if (!Enum.IsDefined(typeof(OrderStatus), input))
{
    throw new ArgumentException("Invalid status value");
}

Flags Without Powers of Two

If you use [Flags] but don't use powers of two, combinations won't work correctly:

[Flags]
public enum BadFlags
{
    First = 1,
    Second = 2,
    Third = 3  // Wrong! Should be 4
}

When you combine First | Second, you get 3, which collides with Third. Stick to 1, 2, 4, 8, 16, and so on.

When Not to Use Enums

Enums are great for fixed sets of values, but they're not always the right choice.

Avoid enums when values change frequently: If you're storing configuration options that users can add or remove, enums become a maintenance burden. Every time you add a value, you need to recompile and redeploy. Consider using a database table or configuration file instead.

Don't use enums for open-ended sets: Country codes, currency codes, or product categories that grow over time are poor fits for enums. You'll be constantly updating the enum definition and creating breaking changes.

Avoid enums for large sets of values: If you have hundreds or thousands of possible values (like error codes across a large system), enums become unwieldy. A constant class or database lookup is more maintainable.

Consider alternatives for extensibility: If you need third parties to extend your enumeration, use abstract classes or interfaces with predefined instances instead:

public abstract class OrderStatus
{
    public static readonly OrderStatus Pending = new PendingStatus();
    public static readonly OrderStatus Shipped = new ShippedStatus();

    public abstract string GetDisplayName();
}

// Third parties can add their own
public class CustomStatus : OrderStatus
{
    public override string GetDisplayName() => "Custom Processing";
}

Conclusion

Enums replace magic numbers with named constants, making your code self-documenting and type-safe. They prevent invalid values at compile time and work seamlessly with IntelliSense and switch expressions. Use basic enums for single-choice scenarios and [Flags] enums for combinations. Always define a meaningful zero value to handle default initialization correctly.

Remember the key pitfalls: validate when converting from integers or strings, use powers of two for flags enums, and avoid enums for frequently changing or open-ended value sets. When used appropriately, enums make your code clearer and more maintainable.

Frequently Asked Questions

Should I use int constants or enums?

Use enums when you have a fixed set of related values that won't change at runtime. Enums provide type safety, IntelliSense support, and self-documenting code. Use int constants only for truly isolated values like buffer sizes or maximum limits that don't form a logical group. Enums prevent invalid values at compile time, while int constants allow any integer assignment.

When should I use the Flags attribute?

Use [Flags] when you need to combine multiple enum values together using bitwise operations. Common scenarios include file permissions, UI states, or configuration options where multiple values can be active simultaneously. Always use powers of two (1, 2, 4, 8, 16) as underlying values. Without [Flags], enums represent a single choice from a set of options.

How do I convert enums to strings safely?

Use ToString() for simple display names, but consider creating extension methods or dictionaries for production-quality string conversion. The ToString() method returns the enum name exactly as written in code, which may not be user-friendly. For parsing strings to enums, always use Enum.TryParse<T>() instead of Enum.Parse() to avoid exceptions. Consider using System.Text.Json attributes like [JsonConverter] for API serialization.

What's the default value for an enum field?

The default value is always 0, even if you don't define a zero value in your enum. This can cause unexpected behavior. Best practice: always define a meaningful zero value like None, Unknown, or Default as the first enum member. Avoid starting enums at 1 unless you have a specific reason, because uninitialized enum fields will be 0 regardless of your enum definition.