Choosing Between Struct, Class, and Collections in ASP.NET Core: A Practical Decision Matrix
2026/04/145 min read
bookmark this
Choosing Between Struct, Class, and Collections in ASP.NET Core: A Practical Decision Matrix
In a typical three-tier ASP.NET Core API (API, Service, Data/Proxy), choosing between struct, class, and different collection types can impact performance, maintainability, and correctness. Here’s a decision matrix with real-world C# examples for each scenario.
How Much Data/Size: Struct vs Class?
General Rule:
- Use
structfor small, immutable value types (typically ≤16 bytes, up to 2–4 fields, no reference-type fields). - Use
classfor larger objects (more than 16 bytes, many fields, or containing reference types).
Why?
- Small structs are fast to copy and store inline (on the stack or inside arrays/objects).
- Large structs (over 16 bytes or 3–4 fields) become expensive to copy, so use
classto avoid performance issues.
Microsoft’s guideline:
"A struct should be less than 16 bytes and ideally immutable. If it’s larger, use a class instead." (.NET docs)
Decision Matrix
| Scenario | Choice | Reasoning |
|---|---|---|
| Small value object (e.g., Money, Point) | struct inside class | Inline storage, zero extra allocations, fast, avoids heap allocation, value semantics |
| Large value object (>16 bytes, many fields) | class inside class | Copying large structs is expensive; class avoids unnecessary copying, reference semantics |
| Collection of small data | List | Compact, cache-friendly, no per-item heap allocation, efficient for value types |
| Collection of large data | List | Avoids copying large objects, allows sharing references, better for polymorphism |
| Shared mutable state | class inside class | Multiple references to same object, enables shared state and mutation |
| Immutable data (readonly) | readonly struct inside class | Best performance + safety, thread-safe, no accidental mutation, value semantics |
1. Small Value Object (e.g., Money, Point)
Use: struct inside a class
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
public class Shape
{
public Point Center { get; set; }
}
Why?
- No heap allocation for
Point. - Value semantics (copy by value).
2. Large Value Object (>16 bytes, many fields)
Use: class inside a class
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public string Country { get; set; }
}
public class Customer
{
public Address ShippingAddress { get; set; }
}
Why?
- Avoids expensive copying of large structs.
- Reference semantics.
3. Collection of Small Data
Use: List<struct>
public struct Pixel
{
public byte R, G, B;
public Pixel(byte r, byte g, byte b) => (R, G, B) = (r, g, b);
}
public class Image
{
public List<Pixel> Pixels { get; set; } = new List<Pixel>();
}
Why?
- Compact, cache-friendly storage.
- No per-item heap allocation.
4. Collection of Large Data
Use: List<class>
public class Document
{
public string Title { get; set; }
public string Content { get; set; }
}
public class Library
{
public List<Document> Documents { get; set; } = new List<Document>();
}
Why?
- Avoids copying large objects.
- Allows sharing and polymorphism.
5. Shared Mutable State
Use: class inside a class
public class Counter
{
public int Value { get; set; }
}
public class Game
{
public Counter Score { get; set; } = new Counter();
}
// Multiple references can share the same Counter instance
Why?
- Multiple references to the same object.
- Enables shared, mutable state.
6. Immutable Data (readonly)
Use: readonly struct inside a class
public readonly struct Temperature
{
public double Celsius { get; }
public Temperature(double celsius) => Celsius = celsius;
}
public class WeatherReport
{
public Temperature Current { get; }
public WeatherReport(Temperature current) => Current = current;
}
Why?
- Thread-safe, no accidental mutation.
- Best performance for small, immutable data.
Summary Table
| Scenario | Use This |
|---|---|
| Small value object | struct inside class |
| Large value object | class inside class |
| Collection of small data | List |
| Collection of large data | List |
| Shared mutable state | class inside class |
| Immutable data | readonly struct inside class |
Tip:
- Use
structfor small, immutable, value-like data (≤16 bytes, no inheritance, no shared state). - Use
classfor large objects, shared/mutable state, or when reference semantics are needed. - Use
readonly structfor immutable value types to prevent accidental mutation. - Use collections of structs for small, frequently accessed data; use collections of classes for large or polymorphic data.
Have questions or want to see more advanced scenarios? Leave a comment below!