
What Is a Record in C#?
A record is a C# type designed primarily for representing data. Unlike a traditional class, where two objects are normally considered equal only when they reference the same instance, records use value-based equality.
For example, consider a simple customer address:
public record Address(
string Street,
string Town,
string Postcode);
We can create two separate instances containing exactly the same information:
var address1 = new Address(
"10 High Street",
"Cardiff",
"CF10 1AA");
var address2 = new Address(
"10 High Street",
"Cardiff",
"CF10 1AA");
Console.WriteLine(address1 == address2);
The result is True.
With a conventional class, these would normally be two separate objects and comparing them with == would compare their references rather than their contents.
This value-based behaviour is one of the main reasons records are useful. Microsoft describes records as types whose primary role is storing data, particularly where two instances containing the same values should be considered equal.
Records Are About Data Rather Than Behaviour
One of the easiest ways to decide whether to use a record is to consider what the type represents.
If the type primarily represents a piece of data, a record is often a good choice.
If the type represents an object with behaviour, responsibilities and a changing state, a traditional class is usually more appropriate.
For example, a product search result could be represented as:
public record ProductSearchResult(
int ProductId,
string Name,
decimal Price);
The object is essentially carrying information from one part of the application to another.
There is very little identity involved. If two search results contain the same product ID, name and price, there is a good argument for considering them equal.
A shopping basket, on the other hand, may have operations such as adding products, removing products, calculating totals and applying discounts. That sort of object has behaviour and state, making a class a more natural choice.
Records and Immutability
Records are particularly useful when you want to create immutable data models.
With positional record syntax, properties on a record class are generated as init-only properties:
public record Customer(
int Id,
string Name,
string Email);
Once a Customer has been created, its positional properties cannot simply be assigned new values.
var customer = new Customer(
1,
"Craig",
"craig@example.com");
Instead of changing the existing object, you can create a new version of it.
This is particularly useful in applications where data should not unexpectedly change after it has been created.
Immutability can make code easier to reason about because a value passed into a method can remain unchanged rather than being modified elsewhere in the application.
Records can technically be mutable, but their design is primarily aimed at data-centric and immutable models.
The with Expression
One of the most useful features of records is the with expression.
Suppose we have:
var customer = new Customer(
1,
"Craig",
"craig@example.com");
If the customer changes their email address, rather than modifying the original record we can create a new record:
var updatedCustomer = customer with
{
Email = "newemail@example.com"
};
The original record remains unchanged.
Console.WriteLine(customer.Email); Console.WriteLine(updatedCustomer.Email);
This approach is often a good fit for applications where objects represent snapshots of data.
It can also make code considerably easier to understand because the original value remains available while the new value is created separately.
Microsoft refers to this as nondestructive mutation because the original record is not modified; a new instance is produced with the requested changes.
Records Are Excellent for DTOs
One of the most practical uses for records is representing Data Transfer Objects, commonly known as DTOs.
For example, an ASP.NET Core API might return:
public record UserDto(
int Id,
string Name,
string Email);
This is an excellent fit because the DTO's purpose is to carry data between layers or across an API boundary.
There is normally little reason for a DTO to contain complex business behaviour or mutable state.
Records also make DTO declarations considerably shorter.
Instead of writing a class containing a constructor, properties and equality implementations, the record can express the intent in a single declaration.
This is one of the areas where records can remove a surprising amount of boilerplate from a modern C# application.
Records and API Requests
Records are also useful for representing incoming API requests.
For example:
public record CreateCustomerRequest(
string Name,
string Email);
An ASP.NET Core endpoint can then accept this type directly:
[HttpPost]
public IActionResult Create(CreateCustomerRequest request)
{
// Create customer
return Ok();
}
The request represents data arriving at the application. It does not need to own the business process for creating the customer.
This separation between data and behaviour is exactly where records tend to work well.
Records and Configuration Data
Another useful scenario is configuration or application settings that should be treated as a collection of values.
For example:
public record ApplicationSettings(
string ApplicationName,
string Environment,
bool EnableLogging);
The record clearly communicates that the type represents a set of related values.
This can be particularly useful when passing configuration or calculated settings between different parts of an application.
Records for Value Objects
Records can also be an excellent choice for value objects.
A value object is defined by its values rather than by a unique identity.
Consider a postcode:
public record Postcode(string Value);
Or a geographical coordinate:
public record Coordinate(
double Latitude,
double Longitude);
Two Coordinate instances containing the same latitude and longitude can naturally be considered equal.
This is a much better semantic match for a record than an object whose identity is important.
Record Class Versus Record Struct
C# provides two main forms of records: record class and record struct.
A normal record is a reference type:
public record Product(
int Id,
string Name,
decimal Price);
This is equivalent to explicitly writing:
public record class Product(
int Id,
string Name,
decimal Price);
A record struct is a value type:
public record struct Coordinate(
double Latitude,
double Longitude);
The choice should be made in much the same way as choosing between a class and a struct.
A record class is generally appropriate when the data should be represented by a reference type, particularly when inheritance may be required or the object is large enough that copying it would be undesirable.
A record struct is more appropriate for small, self-contained values where value-type semantics make sense. Microsoft specifically recommends considering record struct for small values that can be copied efficiently.
When a Class Is Better
Records are useful, but that does not mean every class should be converted into one.
Consider an order:
public class Order
{
public int Id { get; private set; }
public void AddItem(Product product)
{
// Add product to order
}
public void RemoveItem(Product product)
{
// Remove product from order
}
public decimal CalculateTotal()
{
// Calculate total
return 0;
}
}
The order has an identity and behaviour.
It changes throughout its lifetime, and its methods control how that state changes.
This is a strong indication that a class is more appropriate.
The distinction can be summarised simply: records are generally about representing data, while classes are generally about representing objects with identity, behaviour and responsibilities. Microsoft makes the same distinction in its guidance on choosing between records and classes.
Be Careful with Entity Framework Core
One important area where you should generally avoid records is Entity Framework Core entity types.
For example, it may be tempting to define a database entity like this:
public record Customer(
int Id,
string Name,
string Email);
However, EF Core entities have identity and are tracked by the DbContext.
Records use value-based equality, which does not naturally match the way EF Core tracks entities.
For database entities, a normal class is generally the safer and more appropriate choice:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
Microsoft's current guidance specifically recommends avoiding records for Entity Framework Core entity types because EF Core relies on reference equality when tracking entities.
Records and Inheritance
Records can also participate in inheritance hierarchies when using record class.
For example:
public record Person(
string FirstName,
string LastName);
public record Employee(
string FirstName,
string LastName,
int EmployeeNumber) : Person(
FirstName,
LastName);
This can be useful when the data model genuinely benefits from a hierarchy.
Record inheritance also maintains value-based equality semantics, with the runtime type being taken into account when comparing records.
Records Make Equality Much Easier
One of the biggest advantages of records is that you do not have to write the equality boilerplate yourself.
With a traditional class, implementing meaningful value equality can involve overriding Equals, GetHashCode and potentially the equality operators.
Records generate the required equality members for you.
For example:
public record Address(
string Street,
string Town,
string Postcode);
You can then simply write:
if (address1 == address2)
{
// Addresses contain the same values
}
The compiler-generated equality compares the values that make up the record rather than simply asking whether both variables reference the same object.
A Simple Rule for Choosing
When deciding whether to use a record, ask yourself what the type fundamentally represents.
If it represents data, particularly immutable data where equality should be based on the contents, a record is probably worth considering.
If it represents an entity with a unique identity, mutable state and business behaviour, a class is usually the better choice.
For small value types where copying is appropriate, consider a record struct.
For larger reference-based data models, DTOs, API requests and value objects, a record class is often a good fit.
Conclusion
Records are one of the most useful additions to modern C#. They provide concise syntax while giving developers value-based equality, useful ToString() output and convenient nondestructive copying through with expressions.
The important thing is not to think of records as a modern replacement for classes. They solve a different problem.
Use a record when the identity of an object is less important than the data it contains. Use a class when the object has an identity, behaviour or mutable lifecycle that forms an important part of the application.
Once you start thinking in terms of data versus behaviour, choosing between a record and a class becomes much easier.
Become a member
Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!
Read next
Understanding Middleware in ASP.NET Core
Middleware is one of the fundamental building blocks of ASP.NET Core. It sits between an incoming HTTP request and your application, allowing you to inspect, modify or respond to requests before they reach your application code. Understanding how middleware works, and especially the order in which it runs, is essential when building reliable ASP.NET Core applications.
Logging Strategies That Scale
Background Services in ASP.NET Core
