C# Enums and Named States: Clear Choices, Safe Input
Enums make a small set of choices easier to read, but a name is only part of the contract. Learn what values an enum can hold, how to validate external input, and how a model enforces allowed state changes.
What you will understand
- Declare an enum and compare values using meaningful names.
- Explain underlying integer codes and stable external representations.
- Recognize zero defaults and unnamed enum values.
- Map states to friendly labels without changing state.
- Separate TryParse success from declared membership and workflow acceptance.
- Implement a strict name-only input policy.
- Guard a lifecycle transition independently of enum membership.
- Test valid names, unsupported input and refused state changes.
Give a small set of choices clear names
An enum is a custom value type containing named constants. Use one when a value comes from a small, meaningful set of choices, such as a loan status. LoanStatus.Available reads more clearly than an unexplained number one and is harder to confuse with an unrelated count.
This lesson models one physical book copy with one lifecycle status at a time. Available means it can be borrowed, Borrowed means it is already on loan, and Retired means it is out of circulation. Unknown represents an uninitialized or unsupported result, not a usable starting state for our BookCopy.
Naming states does not create behavior. An enum declaration does not borrow a book, validate a user form or prevent every invalid runtime value. We will build those boundaries explicitly so you can see which rule comes from the language and which rule belongs to our model.
Read the first example as a typed assignment followed by a comparison. status is a variable, LoanStatus is its type and Available is a named constant. No new object expression is needed to hold this small value.
Program.cs · Use a state name instead of a magic number
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Available
True
LoanStatus is a custom value type and Available is one of its named constants. The comparison clearly expresses its meaning without requiring the reader to remember what a number such as one represents. Writing the enum does not create a BookCopy or change any loan state.
Program.cs · A string is not automatically a loan status
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: string cannot implicitly convert to LoanStatus.A string spelling and an enum value are different types. External text needs parsing under a stated policy. C# does not silently treat a matching-looking string as the enum member.
Understand the numbers without making them the main interface
By default, an enum uses int underneath. Without explicit values, its first member is zero and each following member increases by one. Our declaration writes every value explicitly so the connection between a name and its code is visible.
Casting a declared value to int reveals that code. Casting an integer to the enum goes the other way, but does not automatically check whether a declared member has that code. The value type can hold unnamed underlying values too, which is why later validation matters.
If you save numeric enum values in files or a database, or exchange them through an API, their meaning becomes a compatibility contract. Reordering implicit members can accidentally change stored meanings. Choose stable codes deliberately and avoid reusing a removed code for a different state.
The underlying number is not an ordering rule. Do not write status > Available to decide which transitions are allowed. Explicitly name the allowed source and destination states. Numeric representation and business meaning answer different questions.
Program.cs · Names have underlying integer values
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
0
2
3
This enum explicitly uses int values zero through three. Without explicit assignments, an ordinary enum starts at zero and increments by one. The cast shows the underlying number. If values are stored externally, choose stable explicit codes and do not casually change their meaning or reuse a retired code.
Program.cs · A cast can create an undefined enum value
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
99
False
The explicit cast succeeds for this int-backed enum but does not prove ninety-nine is a supported status. IsDefined checks membership in the declared constants. Do not mistake a cast for validation or assume a switch will never see an unnamed value.
The default is always zero, even without a zero member
default(LoanStatus), or default in a typed assignment, produces the zero value. We deliberately name that value Unknown. This makes an uninitialized result readable, but does not mean the workflow should accept it as a real loan state.
The DeliveryStage example begins its declared codes at ten. Its default still has code zero, and Enum.IsDefined reports false. Do not assume the first written name is always the default or that every enum value must correspond to a name.
A meaningful zero member is often useful when the domain has an unknown or none concept. If zero genuinely means an active state, document that decision. Either way, still validate values entering a workflow rather than treating default initialization as proof of an accepted user choice.
Our reader initializes its out result to Unknown on refusal, and callers inspect the Boolean before using it. This prevents a failed parse from silently creating an available book. The model also checks its constructor input as a second boundary.
Program.cs · The default enum value is zero
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Unknown
True
default produces the zero value, not the first state you happen to prefer. Unknown deliberately names zero. It is defined, but our BookCopy constructor later rejects it because a usable copy requires a concrete lifecycle state. Defined and acceptable are different checks.
Program.cs · Zero can exist even when it has no name
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
0
False
default is still zero even though no member names it. Enum values are not limited to the declared names at runtime. A meaningful zero member can help express an uninitialized state, but validation must still enforce what the workflow accepts.
Use named comparisons and deliberate display mapping
Compare an enum with its named constants, as in status == LoanStatus.Available. A switch can map technical state names to friendly wording. This keeps display text separate from the names used in the program.
The reporting switch has a fallback because Unknown and unnamed values are possible. It returns Unsupported status for those values instead of pretending they are a valid active state. A production boundary might reject the value earlier, but display code should not accidentally crash or mislabel it.
A friendly label is not a transition rule. Currently on loan tells the reader what the state means, while TryBorrow decides whether an operation may change it. Separating reporting from mutation keeps repeated reads from changing the model.
An ordinary loan lifecycle is one choice at a time. Do not combine Available and Borrowed using bitwise operators. Flags enums are a different design for independent options that may coexist; they require deliberate power-of-two values and different validation policies, so this lesson keeps the lifecycle non-flags.
Program.cs · Translate state to a reader-friendly label
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Ready to borrow
Currently on loan
Removed from circulation
Unsupported status
A switch maps technical state names to display wording. The fallback deliberately handles Unknown and unnamed values in this reporting example. A label is not an authorization decision or a valid transition; the model enforces its own rules.
Conversion success is only the first check
External input arrives as text or a number, not as an automatically trusted state. Enum.TryParse<LoanStatus> attempts to convert text and reports conversion success through a Boolean. The angle brackets specify which enum type is being parsed; you do not need to write a generic class to use this library method.
With ignoreCase: true, borrowed can match Borrowed. But TryParse also accepts numeric strings, so 99 can convert successfully even though no member has that value. After conversion, Enum.IsDefined checks whether the value is one of the declared constants.
Membership is still not the whole workflow. Unknown is declared but is not accepted by our BookCopy constructor. A numeric-capable boundary therefore checks conversion, defined membership and its additional allowed-state policy. Always check the Boolean before treating the out result as accepted input.
TryParse can also interpret comma-separated enum names. That syntax can combine underlying values, even for an enum not marked Flags. For a name-only user field, conversion plus IsDefined may therefore be too permissive. Define the text format you actually accept rather than assuming the parsing helper implements the entire field policy.
Program.cs · TryParse success does not guarantee a declared member
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
borrowed: parsed=True, defined=True
99: parsed=True, defined=False
not-a-status: parsed=False, defined=False
Enum.TryParse can parse names and numeric strings. Ninety-nine fits the underlying int, so parsing succeeds even though no constant names it. The out variable must not be treated as accepted state when parsing fails. The short-circuit check only tests membership after conversion succeeds.
Make a name-only boundary explicit
Our final input field accepts exactly Available, Borrowed or Retired after trimming outside spaces, ignoring case. It rejects numeric strings, comma-separated names, Unknown, empty input and arbitrary words. This is a deliberate product rule, not a universal restriction on every enum parser.
TryReadStatus uses an explicit allowlist of those names. It assigns the corresponding enum only when one accepted word matches. Otherwise it returns false and leaves status as Unknown. The reader does not create a BookCopy, so refusal has no hidden state-changing side effect.
StringComparison.OrdinalIgnoreCase gives these technical input tokens a consistent case-insensitive comparison rather than depending on a user culture. Display labels can still be translated separately. If a public API promises stable numeric codes instead, write and test that different boundary explicitly.
Notice why the repair and lab use different acceptance policies. The guided repair fixes a numeric-capable parser that should reject unsupported codes. The lab implements a stricter name-only reader. Both separate conversion from application policy, but they should not claim to accept the same input language.
Program.cs · Accept only the names promised by this input field
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
borrowed: True, Borrowed
2: False, Unknown
Unknown: False, Unknown
Available, Borrowed: False, Unknown
Retired: True, Retired
This name-only input contract accepts three exact words after trimming, ignoring case. It rejects numbers, Unknown and comma-separated names deliberately. IsDefined alone cannot enforce that textual policy: parsing may accept numeric text or combined names. The explicit allowlist makes the accepted input clear.
A declared status does not automatically allow an action
BookCopy accepts only defined non-Unknown initial states. Available, Borrowed and Retired may each describe an existing copy, but only Available permits a new borrow operation. A defined destination name does not mean every source may move to it.
TryBorrow checks current Status before writing it. An accepted request moves Available to Borrowed and returns true. A second request returns false and leaves Borrowed unchanged. A retired copy likewise refuses borrowing without changing its status.
The public getter and private setter prevent a caller from directly assigning arbitrary state through the supported interface. The constructor and methods must still preserve their own rules. An enum alone cannot enforce this lifecycle because code can manufacture unnamed values with casts.
The example keeps only the borrow transition so the responsibility stays easy to inspect. A complete library workflow might add returns, retirement policies and persistence. Those would require explicit transitions and validation rather than simply adding more enum names.
Program.cs · A valid enum state still needs valid transitions
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
True
Borrowed
False
Borrowed
The constructor rejects undefined and Unknown states. TryBorrow accepts only Available and moves it to Borrowed. Borrowed and Retired are both defined values but cannot be borrowed again through this operation. Enum membership does not implement lifecycle rules automatically.
Test representation, input policy and transitions separately
Check three different layers. First, confirm the named codes and zero behavior. Second, test the boundary that accepts external input. Third, verify allowed and refused state changes. A passing name comparison alone cannot prove all three layers.
For the name-only reader, test every allowed word, mixed case, outside spaces, numbers such as 2 and 99, comma-separated names, Unknown, null and blank input. Successful results must return the right enum; refused results must not create a copy or silently choose Available.
For the model, test Available borrowing successfully, Borrowed and Retired refusing, and a second borrow leaving the state unchanged. Reject Unknown and cast values such as (LoanStatus)99 during construction. The expected operation result and the final state are both part of the check.
Every editor is a standalone local program using source values you can edit. The final lab stores state only in memory; it does not save a library record or reserve stock across users. The next lesson introduces text-file persistence, where external values still need this kind of deliberate validation.
Guided repair: validate beyond parsing
The numeric-capable parser accepts 99 because it equates TryParse success with valid status. Reject undefined codes and Unknown, and do not apply a refused result.
Program.cs · A parsed numeric value slips through a weak boundary
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Accepted: True
Status: 99
The program treats conversion success as acceptance. That admits an unsupported numeric code. This repair targets a numeric-capable parser contract: defined Available, Borrowed and Retired values may be supplied by name or number, but Unknown and undefined codes must be refused.
Repair checkpoints
- Keep conversion as the first check.
- Require Enum.IsDefined(status) after successful parsing.
- Also reject LoanStatus.Unknown under this workflow.
- Only use the parsed status inside the accepted branch.
- Keep this numeric-capable policy distinct from the stricter name-only lab.
Reveal the repair and explanation
Program.cs · Check conversion, membership and workflow policy
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Accepted: False
No status applied
The boundary separates three questions: did conversion succeed, is the value declared, and is it allowed for this workflow? It refuses ninety-nine and does not apply the out value. This is intentionally broader than the name-only reader, which also rejects numeric text and combined-name syntax.
Independent lab: a name-only reader and a guarded loan
Complete a name-only status reader, then create and borrow a copy only after input is accepted. Keep parsing policy and lifecycle enforcement in their separate functions.
Acceptance checklist
- Accept only Available, Borrowed and Retired names, case-insensitively after trimming.
- Reject numeric strings, combined names, Unknown, blank and null input.
- Leave the out value Unknown on refusal and return false.
- Create BookCopy only in the accepted branch.
- Preserve constructor rejection of undefined and Unknown values.
- Preserve TryBorrow accepting only Available and moving it to Borrowed.
- For the reference input, print Start: Available, Borrow accepted: True and After: Borrowed.
Program.cs · Validated loan-status reader starter
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Unsupported status; no copy created
The model already guards creation and borrowing. The input reader is a compilable placeholder that refuses everything. Implement the three-name policy without weakening the model or creating a copy before the reader accepts the input.
Show hints
- Begin by assigning Unknown to the out parameter and checking for blank input.
- Compare the trimmed token against an explicit allowlist of three names.
- Do not use a failed parse result or fall back to Available.
Reveal the reference solution
Program.cs · Validated loan-status reader reference solution
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Start: Available
Borrow accepted: True
After: Borrowed
The name reader accepts Available with outside spaces and different casing. The constructor enforces supported initial states, then TryBorrow applies the allowed transition. Invalid text is refused before object creation. This is temporary local state; no library record is saved or reserved.
Changed-input checks
- Mixed-case available with outside spaces is accepted and can be borrowed.
- Borrowed and Retired names are accepted input but their copies refuse borrowing.
- 2, 99 and Available, Borrowed are refused by the name-only reader.
- Unknown, an empty string, whitespace and null are refused.
- A second borrow is refused and leaves Borrowed unchanged.
- Constructor input Unknown or (LoanStatus)99 is rejected for status.
Recap: representation, input policy and behavior
Identify the code, its declared name, the accepted input format and the transitions the model permits.
enum LoanStatus- Declare a custom value type with named constants.
LoanStatus.Available- Use a meaningful named value rather than a magic number.
(int)status- Inspect the underlying numeric code.
(LoanStatus)99- Convert representation without proving declared membership.
default- Produce zero, whether or not that code has a name.
Enum.TryParse- Attempt name or numeric conversion; success is not full validation.
Enum.IsDefined- Check declared membership, not every workflow or textual-input rule.
guarded transition- Apply an action only from an allowed current state.
Frequently asked questions
Is an enum limited to its written names at runtime?
No. A cast or numeric parse can produce an unnamed underlying value. Check defined membership and the workflow policy at input boundaries.
Does TryParse returning true mean the state is valid?
No. Numeric strings may parse into undefined values. A declared value such as Unknown can also be disallowed by the workflow.
Is the first declared member always the default?
No. The default underlying value is zero. If the first member is explicitly ten and no member is zero, default has no declared name.
Why reject the string 2 when it represents Borrowed?
The lab field deliberately promises name-only input. Another boundary may accept stable numeric codes, but its policy must be stated and tested separately.
Why not use flags for Available and Borrowed?
Those lifecycle states are mutually exclusive. Flags model independent options that may be combined and require a different representation and validation policy.
Do enum names enforce transitions?
No. The model must explicitly check current state before changing it. Defined membership says a value exists, not that an action is allowed.
Can I rename or reorder enum members after storing their numbers?
Treat externally stored codes as a compatibility contract. Explicit stable codes reduce accidental renumbering, but changing names or meanings can still affect serialized data and consumers.
Can you explain which state values are accepted?
Distinguish enum representation, defaults, parsing, membership and lifecycle rules. Read the explanation after checking your answers.
Next step and references
Enums clarify a small set of choices, but casts and parsing can still produce unsupported values. Validate input policy explicitly, then let the model enforce allowed state changes.
Next is C# Read and Write Text Files. You will persist small text data deliberately, distinguish replacing from appending and handle paths and missing files.
Official references
Original SovranCode explanations, examples and exercises. Technical details were checked against these references.
- Microsoft Learn: Enumeration types and numeric representation
- Microsoft Learn: Enum.TryParse conversion behavior
Last updated: .