C# Record vs Class: Choosing the Right Type
Learn the key differences between C# records and classes, including equality, immutability, and when to use each type in your code.
Choosing between a record and a class in C# depends on whether the type represents a value or an identity. Records are a strong fit when data is compared by content and should be immutable; classes remain a good fit for mutable state and object identity. This decision affects equality, mutation, and how the type behaves across API boundaries.
Core Syntax and Declaration
A class is the traditional reference type. You declare properties and often write a constructor, but the compiler does not generate equality members for you. A record (the default record class form) is also a reference type, but the compiler enriches it with value-based equality, a ToString override, and a with expression for non-destructive mutation.
public class PointClass { public int X { get; set; } public int Y { get; set; } public PointClass(int x, int y) { X = x; Y = y; } } public record PointRecord(int X, int Y);
The positional record syntax is compact, but it is not the only option. You can write a record with a traditional property body and still get the synthesized equality behavior. The key difference is that the compiler generates Equals, GetHashCode, and == for records based on the declared properties.
Value Equality vs Reference Equality
For classes, equality is reference-based by default. Two separate instances with identical property values are not considered equal unless you override Equals and GetHashCode. Records, on the other hand, implement structural equality. Two records with the same property values are equal.
var a = new PointClass(1, 2); var b = new PointClass(1, 2); Console.WriteLine(a == b); // False var c = new PointRecord(1, 2); var d = new PointRecord(1, 2); Console.WriteLine(c == d); // True
This behavior is useful when the identity of the data is determined by its content, not by its location in memory. For a DTO or a domain value object, this is often exactly what you want. For an entity with a database ID, reference equality or a custom equality implementation is usually more appropriate.
Immutability and with Expressions
Records are commonly used to model immutable data. The positional syntax creates init-only properties, meaning you can set them during construction but not change them afterward. If you need a record with mutable properties, you can declare set accessors explicitly, but that removes the default immutability benefit. To modify an immutable record, you use the with expression, which creates a new instance with specified properties changed.
var original = new PointRecord(1, 2); var moved = original with { X = 3 }; Console.WriteLine(original); // PointRecord { X = 1, Y = 2 } Console.WriteLine(moved); // PointRecord { X = 3, Y = 2 }
Classes do not have this built-in. You either make all properties init-only and write a copy method yourself, or you allow mutation and risk unintended changes. The with expression is a compile-time feature that uses the generated copy constructor, so it is efficient and type-safe.
Inheritance and Sealing
Both class and record support inheritance, but records have additional constraints. A record can inherit from another record, but not from a non-record class. The base type in a record hierarchy must also be a record. You can seal a record to prevent further derivation.
public record Shape; public record Circle(double Radius) : Shape; public sealed record Square(double Side) : Shape;
Classes are more flexible in inheritance scenarios. You can have a class hierarchy that mixes abstract classes, interfaces, and concrete classes without the record-specific restrictions. If you need to model a polymorphic hierarchy where the base type is a non-record class, a class is the only option.
Performance and Runtime Behavior
Because record classes are reference types, they are allocated on the heap just like classes. The synthesized equality methods are generated at compile time and compare each declared field, using the same comparison mechanisms as manually written equality members. For most applications, the difference is negligible. However, if you are comparing large collections of objects frequently, a custom equality implementation on a class might be more efficient if you can short-circuit on a single field.
Records also generate a ToString method that prints property names and values. This is helpful for logging and debugging, but it can expose sensitive data if you are not careful. Classes do not get this automatically; you must override ToString manually.
When to Use a Record Over a Class
Use a record when the type represents a value, such as a coordinate, a configuration option, or a data transfer object. The structural equality and immutability make records a natural fit for data that is compared by content and passed between layers without mutation.
Use a class when the type has identity, such as an entity in a domain model with a primary key, or when you need to manage mutable state that changes over time. Classes also remain the right choice when you need to inherit from a non-record base class or when you rely on reference equality for object identity.
The decision also depends on how you consume the type. If you are using an ORM that requires parameterless constructors and mutable properties, a record with init-only properties may not work without additional configuration. In such cases, a class is simpler.
Practical Example: DTO vs Domain Entity
Consider a REST API that returns a product. The DTO is a perfect record candidate because it is immutable, compared by value, and serialized easily.
public record ProductDto(int Id, string Name, decimal Price);
The domain entity, on the other hand, might have a database ID and mutable stock level. A class gives you the flexibility to change the stock level without creating a new instance.
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int Stock { get; set; } }
Mixing both is common: use records for input/output contracts and classes for internal stateful models. This keeps the boundary clean and avoids accidental mutation of data that should be immutable.
Compatibility and Tooling
Records were introduced in C# 9 and require .NET 5 or later. If you are working on a legacy codebase targeting .NET Framework, records are not available. Classes work everywhere. Before adopting records, verify that your project's target framework and compiler version support them. Modern .NET projects can use records without issue, but you may need to update tooling if you are on an older version.
Also note that records are not automatically serializable in a special way. They work with System.Text.Json and Newtonsoft.Json just like classes, but the init-only properties require that the serializer can set them during deserialization. Both major serializers support this, but you should test your specific setup.
The choice between these types ultimately comes down to whether the type represents a value or an identity. Records give you structural equality and immutability for free; classes give you mutable state and inheritance flexibility. Evaluate the role of the type in your application, and choose the one that matches that role without forcing extra work.