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 struct for small, immutable value types (typically ≤16 bytes, up to 2–4 fields, no reference-type fields).
  • Use class for 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 class to 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 struct for small, immutable, value-like data (≤16 bytes, no inheritance, no shared state).
  • Use class for large objects, shared/mutable state, or when reference semantics are needed.
  • Use readonly struct for 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!