A C# enum gives numbers readable names. It does not guarantee that every value has a name, that parsing proves validity, or that inserting a declaration preserves an older numeric mapping. Those distinctions matter whenever a value survives beyond one execution or moves between independently updated components.

The design rule: assign stable numeric values to durable contracts, define what zero means, and separate parsing from membership and contextual acceptance. Treat a single-choice enum differently from a set of flags. The examples below use fictional document stages and display options, with no database or external application connection.

Preserve numeric meaning rather than declaration order

An implicitly numbered enum can change the meaning of existing integers when a member is inserted before other members. Once numbers are exchanged or retained, preserve their established mappings explicitly and test those mappings directly. Declaration order is a maintenance convenience, not a compatibility contract.

Microsoft's C# reference specifies that the default underlying type is int. Without explicit assignments, numbering begins at zero and advances through the declarations. A source edit that looks cosmetic can therefore change what an older integer means to a newer program. The compiler can accept the edit even though the interpretation of retained data has changed.

The lab contrasts two imaginary definitions. In the old implicit definition, Ready has value one. Inserting Paused before it moves Ready to two. Nothing changed the old value held by a consumer; only the new interpretation changed. That is the important failure mode: a valid integer can silently acquire the wrong meaning.

Appending a member at the end avoids that particular shift, but it leaves the contract vulnerable to later rearrangement. For a durable contract, first establish the historical mapping from reliable evidence, then write explicit assignments. Do not renumber everything from zero merely to make the declarations tidy. An explicit but historically incorrect mapping is still incorrect.

In the stable example, Ready = 10 remains ten after introducing Paused = 5. The gap between numbers is optional. Compatibility comes from keeping existing meanings, not from leaving empty slots. If two consumers already disagree about a mapping, assignments alone cannot reconcile them; a separate version-conversion specification is needed. This article makes no changes to stored data.

Make zero a deliberate policy

The default value of an enum is zero even when no member is assigned zero. Define whether that represents an unspecified state or an acceptable choice, then enforce that policy at the relevant boundary. A named zero value improves clarity but does not automatically make it appropriate for every operation.

default(NoZero) does not choose the first declared member. It also does not choose the lowest declared numeric value. In this lab, NoZero defines ten and twenty, yet its default is zero and Enum.IsDefined returns false. Object construction can therefore produce an unnamed enum value without throwing an exception.

For a new design, Unspecified = 0 gives the default a readable meaning. Whether it should be accepted remains a separate question. An interface that requires an explicit document stage should reject an unspecified value, even though the enum defines it. The sample acceptance function demonstrates that distinction by checking parsing, membership, and the nonzero policy separately.

If omitted input must be distinguished from an explicit zero, consider a nullable value or an explicit presence indicator in the contract. That decision belongs in the data model, not in display formatting. This is not a recommendation to add zero to every older enum without review: existing consumers may already interpret zero differently, and those interpretations need to be understood first.

Parsing success is not membership validation

Enum.TryParse can accept a numeric string whose value has no declared enum member. For a single-choice input, validate membership after parsing and then apply the operation's acceptance rules. If an interface accepts names only, constrain the permitted input forms as well; membership validation alone also permits recognized numeric values.

The expression Enum.TryParse("99", out var value) succeeds in the supplied example, but Stage does not define ninety-nine. This is documented behavior rather than a parsing defect. The method establishes that the input can be represented as the enum's underlying value. It does not establish that the application recognizes or permits that value.

Likewise, casting an integer to an enum is not validation. A typed variable can carry an unnamed underlying value. Treat a cast as a representation operation and place acceptance checks where untrusted or versioned input crosses into application logic. The word “typed” should not become a substitute for an explicit list of accepted states.

The sample accepts both Ready and 10. It rejects 99, 0, and missing, each for a different reason. These choices are intentional, visible policy rather than universal recommendations. A names-only format needs a name allowlist or equivalent input restriction. A numeric-only format should define integer syntax and range handling before applying enum membership rules.

Case sensitivity deserves its own decision. The lab uses the generic overload without enabling case-insensitive parsing. It does not claim that arbitrary capitalization is accepted. A contract review should also specify whitespace handling and aliases where relevant. Widening or narrowing accepted input during a refactor can change compatibility even if every declared member keeps its number.

Validate flags with an allowed-bit policy

A valid combination of flags does not necessarily have its own declared member, so Enum.IsDefined can reject a combination the application permits. Check for unknown bits using an allowed mask, then apply any rules for zero and mutually exclusive options. Bit membership and meaningful combinations answer different questions.

The display example declares Grid = 1 and Labels = 2. Their combination has value three, but there is no named member with value three. Enum.IsDefined consequently returns false. If the interface allows grid lines and labels together, that combination can still satisfy the intended contract. Microsoft's API reference explicitly illustrates this distinction for flag combinations.

The lab checks (combined & ~allowed) == 0. When the allowed mask combines one and two, three contains no unknown bits, while eight contains a bit outside the allowed set. This rule does not decide whether two individually permitted options are mutually exclusive. Nor does it decide whether an empty selection is meaningful. Both conditions need additional policy where applicable.

This example deliberately uses small, nonnegative int flags. It is not a generic conversion utility for every underlying enum type. Signed values, reserved bits, and different integer widths require their own edge cases. First establish whether the model is a single choice or a combination of choices; only then select the validation rule.

Verification: reproduce the contract in a pinned local project

Run the checks in an isolated writable directory with a pinned SDK and complete source files. Each assertion should expose a specific contract and stop the program on failure. The lab creates local build artifacts only; the application has no external service calls and needs no extra package references.

Prerequisite: install .NET SDK 10.0.400 and make dotnet available in the terminal. The project targets net10.0. The global.json file pins SDK selection, not every runtime patch that could exist on another machine. This is a concrete toolchain reproduction, not a claim that a cross-version compatibility matrix has been executed.

  1. Create a new empty directory named enum-contract-lab in a writable location and enter it. Using an editor, save the three complete files below as global.json, EnumLab.csproj, and Program.cs. Do not add them to an existing application project.
  2. Run dotnet --version from that directory. Continue only if the result is 10.0.400. If the SDK is missing or the version differs, stop and resolve the prerequisite rather than removing the version pin.
  3. Run dotnet run --project EnumLab.csproj --configuration Release --no-launch-profile. Passing requires six PASS: lines, a final ALL_CHECKS_PASSED marker, and exit code zero. A build error, a FAIL: exception, or a nonzero exit code is a failure.

Save this as global.json:

{"sdk":{"version":"10.0.400","rollForward":"disable"}}

Save this as EnumLab.csproj:

Exenet10.0enableenabletrue

Save this as Program.cs:

using System;

static class Program
{
    static void Check(bool condition, string label)
    {
        if (!condition) throw new InvalidOperationException("FAIL:" + label);
        Console.WriteLine("PASS:" + label);
    }
    static bool Accept(string text, out Stage stage) =>
        Enum.TryParse(text, out stage) && Enum.IsDefined(stage) && stage != Stage.Unspecified;
    static void Main()
    {
        Check((int)OldImplicit.Ready == 1 && (int)InsertedImplicit.Ready == 2,
            "implicit-insertion-drifts");
        Check((int)Stage.Ready == 10 && (int)ExtendedStage.Ready == 10,
            "explicit-values-stable");
        Check((int)default(NoZero) == 0 && !Enum.IsDefined(default(NoZero)),
            "default-zero-undefined");
        Check(Enum.TryParse("99", out var unknown) && !Enum.IsDefined(unknown),
            "parse-does-not-validate-membership");
        Check(Accept("Ready", out _) && Accept("10", out _) &&
              !Accept("99", out _) && !Accept("0", out _) && !Accept("missing", out _),
            "single-choice-boundary");
        var combined = ViewOptions.Grid | ViewOptions.Labels;
        var allowed = ViewOptions.Grid | ViewOptions.Labels;
        Check(!Enum.IsDefined(combined) && (combined & ~allowed) == 0 &&
              (((ViewOptions)8) & ~allowed) != 0, "flags-mask-not-membership");
        Console.WriteLine("ALL_CHECKS_PASSED");
    }
}
enum OldImplicit { Unspecified, Ready, Done }
enum InsertedImplicit { Unspecified, Paused, Ready, Done }
enum NoZero { Ready = 10, Done = 20 }
enum Stage { Unspecified = 0, Ready = 10, Done = 20 }
enum ExtendedStage { Unspecified = 0, Paused = 5, Ready = 10, Done = 20 }
[Flags] enum ViewOptions { None = 0, Grid = 1, Labels = 2 }

After saving the files, these are the complete execution commands, run inside the new directory:

dotnet --version
dotnet run --project EnumLab.csproj --configuration Release --no-launch-profile

The run command builds as needed and performs implicit restore. It can create bin and obj, and the toolchain may inspect configured package sources. There are no extra package references in this lab, and the sample application itself does not issue network requests. Keeping toolchain behavior separate from application behavior avoids overstating the experiment as an entirely network-disabled build.

Executed scheduler result: the Release command ran on the local Windows environment with SDK 10.0.400, returned exit code zero, and reported all six assertions as passing followed by ALL_CHECKS_PASSED. This is a correctness result. No latency, allocation, throughput, or comparative performance measurements were taken, and no historical figures from the notes were repurposed as test results.

An optional negative control is to change the value of ExtendedStage.Ready in a separate copy of this lab and run it again. The stable-value assertion should fail. That additional negative-control execution was not performed in this scheduler run and is not included among the verified checks.

Review compatibility as a set of boundaries

An enum compatibility review should protect stable numbers, accepted input forms, and unknown-value handling together. Compilation alone does not cover these boundaries. Tests should preserve known mappings and rejection rules, while documentation identifies formats, versions, and consumers that remain outside the verified scope.

Start by asking whether the value ever leaves the current process. A short-lived internal helper may need a much smaller contract than an export format retained across software versions. For the latter, maintain a mapping table containing each name, its fixed integer, the meaning of zero, the retirement policy, and the kinds of consumers that can receive new members. The table need not contain any actual application records.

For important existing values, assert the actual integer rather than merely counting enum members. A member-count assertion can pass while meanings move. Conversely, adding a legitimate new member should not invalidate a test whose purpose is to preserve the numeric identity of an older one. Write assertions around the invariant you intend to protect.

Unknown values also need an explicit path. Depending on the format, a consumer might reject them, isolate them, or preserve the raw value until a newer reader can interpret it. None of those choices should silently convert an unknown value into an unrelated known state. The correct choice depends on the exchange contract, not on which conversion requires the fewest lines.

Names can form a separate contract. If an exchange format uses names, renaming a member can break compatibility even when its integer stays fixed. The tests here do not verify serializer settings, storage providers, or consumers written in another language. Numeric stability is necessary for a numeric contract but is not proof that every surrounding representation is compatible.

Use a compact review checklist:

  • Preserve the integer meaning of each established member.
  • Define whether zero is unspecified, permitted, or rejected at each boundary.
  • Test an unknown numeric value and an unknown name separately.
  • Distinguish single-choice membership from flags-mask validation.
  • Specify input forms and any case-handling policy.
  • Add format-specific and consumer-specific tests before making wider compatibility claims.

This is an informational guide for readers investigating C# enum defaults and parsing validation. Its concern is the stability of numeric meaning, not general exception diagnostics or database schema changes. The practical next step is to add a small contract lab for the values you own, then decide whether an exchange format needs a separately reviewed change.

Sources and verified scope

The article takes its initial problem from the enum section of the development notes and checks the public language concepts against Microsoft documentation and an independent local program. Environment-specific identifiers, unrelated operational material, and historical outcomes are excluded. Both language editions share the same source set and verified facts.

Source provenance: 軟體開發/專案開發問題集/CSharp與DotNet/CSharp 語言與建置陷阱.md. The complete source file was read, but only its numeric-enum and default-value concepts were selected. Other sections of that source are not presented as claims verified by this article.