C# Enums, Records, Structs, and Classes
Choose a type shape based on identity, value equality, mutability, and valid states instead of defaulting every model to a class.
Before this lesson
- Classes, objects, constructors, and properties
- Value and reference behavior
Replace magic status strings with an enum when the set is closed
Distinguish identity equality from value equality
Choose class, record, or struct from behavior and semantics
The short answer
Use an enum for one choice from a small named set, a record for value-like data with concise value equality, a small usually immutable struct for value semantics, and a class for identity-bearing or mutable entities and services.
A named set prevents impossible spellings
A string status can contain "paid", "Paid", "padi", or any other text. When the domain has a small closed set of choices, an enum gives each choice a name and lets the compiler reject unknown members.
Enums are represented by integral values underneath, but do not treat arbitrary integers as automatically valid enum members. Validate values parsed from external input, and avoid relying on numeric order unless the domain explicitly defines it.
OrderStatus status = OrderStatus.Shipped;
switch (status)
{
case OrderStatus.Pending:
Console.WriteLine("Await payment");
break;
case OrderStatus.Paid:
Console.WriteLine("Pack order");
break;
case OrderStatus.Shipped:
Console.WriteLine("Track delivery");
break;
case OrderStatus.Cancelled:
Console.WriteLine("No action");
break;
}Identity and value answer different equality questions
Two bank accounts with the same balance are not the same account; identity matters. Two coordinate values with the same X and Y usually represent the same coordinate; their contained values matter. Type choice should follow that semantic question before performance folklore.
Classes use reference identity for default equality unless they override it. Records are designed for data-centric models and synthesize value-based equality from their components. Modern records also support concise non-destructive copying with with expressions.
// Modern C# / current .NET SDK example
public record ShippingAddress(string City, string Country);
ShippingAddress first = new ShippingAddress("Jeddah", "Saudi Arabia");
ShippingAddress second = new ShippingAddress("Jeddah", "Saudi Arabia");
Console.WriteLine(first == second); // True: same component valuesStructs are copied as values
A struct is a value type. Assigning it to another variable copies its current value. Structs work well for small value-like concepts such as coordinates, ranges, or measurements, especially when immutable. Large or highly mutable structs create surprising copies and are usually a poor fit.
A record struct combines value-type copying with record-style generated value equality. A record class remains a reference type. The word record describes data-oriented behavior; it does not by itself tell you whether storage uses value or reference semantics.
| Type | Typical semantic | Default/canonical use |
|---|---|---|
| enum | One named choice | OrderStatus, Direction |
| class | Identity or mutable collaborator | Customer, BankAccount, service |
| record class | Reference-based data value | Message, request, configuration snapshot |
| struct | Small copied value | Coordinate, measurement |
| record struct | Small data value with generated equality | Immutable value tuple-like model |
Alternatives keep models honest
An enum is not ideal when choices are configured at runtime or each option has substantial changing behavior. A lookup table or polymorphic objects may model those cases better. A record is not automatically immutable if its members are mutable; design the member types and accessors accordingly.
Start with the simplest representation that preserves valid states and equality meaning. Changing a public type later can affect callers, so spend a moment on semantics, but do not create elaborate hierarchies for imagined future requirements.
Good habits
- Use singular enum type names and meaningful member names.
- Prefer immutable structs; avoid setters that mutate copied values unexpectedly.
- Do not select struct solely because you assume it is faster; measure real hot paths.
- Use records for data semantics, not for every class with properties.
Quick knowledge check
Answer before you reveal.
01Are two class objects with equal property values equal by default?
Not usually. Default class equality is reference identity unless the type defines value equality.
02Does record always mean value type?
No. record class is a reference type; record struct is a value type.
Practice challenge
Now build it without copying.
Model a support ticket priority as an enum and a small immutable Money value as a struct with Amount and Currency. Print both and compare two Money values after implementing or generating appropriate equality.
You are done when
- Priority cannot contain an arbitrary spelling
- Money cannot be partially initialized through public mutation
- Your equality choice matches value semantics
Stretch: Using a current local .NET SDK, replace the Money struct with a readonly record struct and compare the amount of code and behavior.
Open challenge in playgroundLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Why does the BuildQuill compiler not run record examples?
Its execution service uses an older C# compiler. Records require a modern local .NET SDK; the lesson marks those snippets as reference examples instead of runnable browser code.
Can an enum combine flags?
Yes, with [Flags] and powers-of-two values when combinations are meaningful. Do not use flags for choices that should be mutually exclusive.
Should database entities be records?
Often entities need stable identity and controlled mutation, which suit classes. Records can fit immutable snapshots and messages. Let domain semantics and framework behavior guide the choice.