Records vs Classes vs Structs in C#
Problem
When designing data types in C#, developers often face the choice between record
, class
, and struct
types. This decision is particularly important for:
- Data Transfer Objects (DTOs) that move data between controller and service layers
- Request bindings in ASP.NET APIs where immutability is desirable
- Any data-centric types where value semantics and equality are important
Solution Overview
C# provides three primary constructs for creating custom types, each with distinct characteristics:
- Records: Reference types with value semantics, ideal for immutable data
- Classes: Reference types for object-oriented hierarchies with behavior
- Structs: Value types for small, efficient data structures
When to Use Each Type
// Best for immutable data with value semantics
public record SearchParameters(
string Query,
int PageSize,
int PageNumber);
// Best for objects with behavior and responsibilities
public class SearchService
{
public async Task<SearchResults> SearchAsync(SearchParameters parameters)
{
// Implementation with behavior
}
}
// Best for small, simple value types
public struct Coordinate
{
public double Latitude { get; }
public double Longitude { get; }
public Coordinate(double lat, double lon)
{
Latitude = lat;
Longitude = lon;
}
}
Detailed Comparison
Record Types
Records are reference types designed for data-centric scenarios with value-based equality:
Key Features
- Value semantics: Equality comparison based on content, not reference
- Immutability by default: Properties are init-only unless specified otherwise
- Non-destructive mutation:
with
expressions create modified copies - Built-in formatting: ToString() provides helpful output
- Inheritance support: Records can inherit from other records
Perfect use cases for records:
- API request/response DTOs
- Configuration objects
- Value objects in domain-driven design
- Any immutable data carrier
Class Types
Classes are traditional reference types focused on behavior and responsibilities:
Key Features
- Reference semantics: Equality comparison based on reference
- Mutability: Properties can be mutable by design
- Full OOP support: Inheritance, polymorphism, interfaces
- Rich behavior: Methods, events, and complex logic
Use classes when:
- You need to define responsibilities and behavior
- Object identity matters more than value equality
- You require complex inheritance hierarchies
- Mutability is part of the design
Struct Types
Structs are value types that live on the stack and are copied by value:
Key Features
- Value semantics: Copied when passed or assigned
- Stack allocation: Generally more memory efficient
- No inheritance: Cannot inherit from other structs or classes
- Limited polymorphism: Can implement interfaces
Struct Limitations
Structs should typically be small (under 16 bytes) and immutable. Large structs can cause performance issues due to copying overhead.
Use structs when:
- The type represents a single value (like primitive types)
- Instance size is under 16 bytes
- Immutability is desired
- Boxing will be infrequent
- Performance is critical (games, high-throughput systems)
Performance Considerations
Records introduce some performance overhead compared to classes due to their value semantics and generated equality methods. However, for most application scenarios, this overhead is negligible compared to operations like database access or network calls.
Performance Test Results
// Sample benchmark comparing records vs classes
[Benchmark]
public int TestRecord()
{
var foo = new Foo("a");
for (int i = 0; i < 10000; i++)
{
var bar = foo with { Bar = "b" };
bar.MutableProperty = i;
foo.MutableProperty += bar.MutableProperty;
}
return foo.MutableProperty;
}
[Benchmark]
public int TestClass()
{
var foo = new FooClass("a");
for (int i = 0; i < 10000; i++)
{
var bar = new FooClass("b") { MutableProperty = i };
foo.MutableProperty += bar.MutableProperty;
}
return foo.MutableProperty;
}
Results:
- Record: ~120.19 μs
- Class: ~98.91 μs
Practical Recommendations
For Your Specific Questions
Should I use Record for DTOs?
✅ Yes, records are ideal for DTOs due to their immutability and value semantics.Should I use Record for request bindings?
✅ Yes, immutable request objects prevent unexpected modifications and ensure consistency.Should SearchParameters be a Record?
✅ Yes, search parameters represent immutable data with value-based equality.
Decision Flowchart
Advanced Scenarios
For performance-critical applications, consider these additional options:
readonly struct
: For immutable value types with guaranteed stack allocationrecord struct
(C# 10+): Combines record features with struct performance benefits
Common Pitfalls
Reference Type Copy Behavior
While records provide value semantics, they are still reference types. Copying with with
expressions creates shallow copies of reference-type members:
var original = new SomeRecord(new List<string>());
var copy = original with { };
// Both point to the same list instance!
original.List.Add("item");
// copy.List now contains "item" too
Encapsulation Limitations
Records have less encapsulation control than classes. They automatically generate public properties for positional parameters and cannot hide parameterless constructors like structs.
Conclusion
Choose your data types based on these guidelines:
- Use
record
for immutable data transfer objects, request bindings, and value-like types - Use
class
for objects with behavior, mutable state, and complex inheritance - Use
struct
for small, simple value types where performance is critical
For your specific scenario with SearchParameters
, a record is the optimal choice as it provides immutability, value semantics, and concise syntax perfect for data transfer scenarios.