Enums

This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

An enum is a value type whose values are named constants over an integral type. It gives a fixed set of options a name, a type and IntelliSense — everything a bare int or string does not.

Declaring an Enum

public enum LogLevel
{
    Trace,      // 0 -- values start at zero and increment
    Debug,      // 1
    Information,// 2
    Warning,    // 3
    Error,      // 4
    Critical,   // 5
}

public static class EnumBasics
{
    public static void Run()
    {
        LogLevel level = LogLevel.Warning;

        Console.WriteLine(level);            // Warning -- ToString gives the name
        Console.WriteLine((int)level);       // 3
        Console.WriteLine(level > LogLevel.Information);   // True -- comparison works
    }
}

The underlying type is int unless you say otherwise:

// A narrower underlying type -- useful for interop and dense storage.
public enum Status : byte
{
    Unknown = 0,
    Active = 1,
    Suspended = 2,
    Closed = 255,
}

// Values may be explicit, repeated (aliases) and out of order.
public enum HttpStatus
{
    Ok = 200,
    Created = 201,
    NoContent = 204,
    MovedPermanently = 301,
    BadRequest = 400,
    NotFound = 404,
    Gone = 410,
    ServerError = 500,

    Success = Ok,          // an alias for the same value
}

Permitted underlying types are byte, sbyte, short, ushort, int, uint, long and ulong.

An enum variable is not restricted to its declared members: (LogLevel)99 is legal and compiles without a warning, because enums are just integers underneath. Validate values that come from outside your code with Enum.IsDefined or a switch with a default arm.

[Flags] Enums

When the values are independent bits that combine, mark the enum [Flags] and give each member a distinct power of two:

[Flags]
public enum FileAccess2
{
    None    = 0,
    Read    = 1 << 0,   // 1
    Write   = 1 << 1,   // 2
    Execute = 1 << 2,   // 4
    Delete  = 1 << 3,   // 8

    ReadWrite = Read | Write,               // a named combination
    All = Read | Write | Execute | Delete,  // 15
}

public static class FlagsDemo
{
    public static void Run()
    {
        FileAccess2 access = FileAccess2.Read | FileAccess2.Write;

        Console.WriteLine(access);                      // Read, Write -- [Flags] ToString
        Console.WriteLine((int)access);                 // 3

        // Testing a flag -- the fast, allocation-free form.
        Console.WriteLine((access & FileAccess2.Write) != 0);      // True

        // HasFlag reads better; it boxes on older runtimes but is optimised now.
        Console.WriteLine(access.HasFlag(FileAccess2.Write));      // True
        Console.WriteLine(access.HasFlag(FileAccess2.ReadWrite));  // True -- ALL bits must be set

        // Adding and removing flags.
        access |= FileAccess2.Execute;                             // add
        access &= ~FileAccess2.Read;                               // remove
        access ^= FileAccess2.Delete;                              // toggle
        Console.WriteLine(access);

        Console.WriteLine(FileAccess2.None == 0);                  // True
    }
}

Always include a zero member named None — it is the value of a default variable, and HasFlag(None) is always true, which is rarely what the caller means.

Converting

public static class EnumConversion
{
    public static void Run()
    {
        // enum <-> integer: explicit casts, both directions.
        LogLevel level = (LogLevel)4;
        int number = (int)LogLevel.Error;
        Console.WriteLine($"{level} {number}");

        // enum -> string
        Console.WriteLine(LogLevel.Warning.ToString());        // Warning
        Console.WriteLine($"{LogLevel.Warning:D}");            // 3 -- decimal format
        Console.WriteLine($"{LogLevel.Warning:G}");            // Warning
        Console.WriteLine($"{FileAccess2.ReadWrite:F}");       // Read, Write -- flags format

        // string -> enum: prefer TryParse for untrusted input.
        if (Enum.TryParse("Warning", ignoreCase: true, out LogLevel parsed))
        {
            Console.WriteLine(parsed);
        }

        // Parse throws on failure; both accept numeric text too, which is a common trap:
        Console.WriteLine(Enum.Parse<LogLevel>("3"));          // Warning -- from "3"!
        Console.WriteLine(Enum.TryParse("99", out LogLevel outOfRange));  // True (!)
        Console.WriteLine(Enum.IsDefined(outOfRange));         // False -- the real check

        // Enumerating members.
        foreach (LogLevel value in Enum.GetValues<LogLevel>())
        {
            Console.Write($"{value}={(int)value} ");
        }
        Console.WriteLine();

        Console.WriteLine(string.Join(",", Enum.GetNames<LogLevel>()));
    }
}

Enum.GetValues<T>() and Enum.GetNames<T>() are the generic (allocation-friendlier) overloads; the non-generic Enum.GetValues(typeof(T)) returns an Array you must cast.

Switching over Enums

public static class EnumSwitching
{
    public static ConsoleColor ColourFor(LogLevel level) => level switch
    {
        LogLevel.Trace or LogLevel.Debug => ConsoleColor.Gray,
        LogLevel.Information => ConsoleColor.White,
        LogLevel.Warning => ConsoleColor.Yellow,
        LogLevel.Error or LogLevel.Critical => ConsoleColor.Red,
        _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown log level."),
    };

    // Relational patterns work because enums are ordered.
    public static bool ShouldAlert(LogLevel level) => level switch
    {
        >= LogLevel.Error => true,
        _ => false,
    };
}

The compiler warns (CS8509) if a switch expression over an enum omits a declared member, which makes adding a member a compile-time prompt to handle it. Keep the _ arm anyway — it catches the out-of-range casts above. For a [Flags] enum, a switch on the whole value is usually wrong; test the bits instead.

Enums in Generics

public static class EnumHelpers
{
    // `struct, Enum` is the constraint that means "any enum type".
    public static IReadOnlyList<T> AllValuesOf<T>() where T : struct, Enum
        => Enum.GetValues<T>();

    public static bool IsValid<T>(T value) where T : struct, Enum
        => Enum.IsDefined(value);

    public static T ParseOrDefault<T>(string text, T fallback) where T : struct, Enum
        => Enum.TryParse(text, ignoreCase: true, out T parsed) && Enum.IsDefined(parsed)
            ? parsed
            : fallback;

    public static void Demo()
    {
        Console.WriteLine(AllValuesOf<LogLevel>().Count);                  // 6
        Console.WriteLine(IsValid((LogLevel)99));                          // False
        Console.WriteLine(ParseOrDefault("critical", LogLevel.Information));
    }
}

Best Practices

  • Name the type in the singular (LogLevel, Status) — except a [Flags] enum, which takes a plural (FileAccess, RegexOptions).

  • Do not prefix members with the type name: LogLevel.Error, not LogLevel.LogLevelError.

  • Give the zero value a meaning. default(T) is zero, so member zero should be None, Unknown or the sensible default — never an arbitrary real option.

  • Pin the numeric values on anything persisted or sent over a wire. If the values are stored in a database or serialised, reordering members silently changes their meaning; assign explicit numbers and never reuse one.

  • Avoid [Flags] unless the values genuinely combine. If only one option can apply at a time, a plain enum is clearer and switchable.

  • Prefer int as the underlying type unless interop or storage density demands otherwise — narrower types save nothing in a local and complicate arithmetic.

  • Validate at the boundary. Cast an int from a database or a request into an enum and you have an unvalidated value; run it through Enum.IsDefined (or a switch that throws) first.

  • When behaviour, not just a label, differs per value, consider a sealed record hierarchy or a dictionary of delegates instead — an enum plus a growing switch in five places is a sign the type wants to be polymorphic.

See Also