Getting Started with C# Records
Anton Martyniuk
Posted on April 21, 2024
Introduction to C# Records
C# 9.0 introduced a new type of class in called record.
Record is a reference type that offers immutability and equality comparison out of the box.
There are 2 forms of defining a record: a classic and concise. Let's have a look on the classic form first:
public record Product
{
public int Id { get; init; }
public string Name { get; init; }
public decimal Price { get; init; }
}
// Create an instance of Product's record
var product = new Product
{
Id = 1,
Name = "Phone",
Price = 500.00
};
This type of declaration is similar to regular C# classes, but instead of a class keyword - a new record keyword is being used.
The best practise when creating a record is declaring all of its properties as init only. This way properties can't be changed, ensuring the immutability of the record's data. Although you can define properties as a regular get-set if needed.
A new keyword called required was introduced in C# 11, which is a great addition to records in my opinion:
public record Product
{
public required int Id { get; set; }
public required string Name { get; set; }
public required decimal Price { get; set; }
}
This ensures that all properties marked with required keyword, should be assigned when creating an object, otherwise a compilation error is raised:
// This code doesn't compile as Price property is not assigned with a value
var product = new Product
{
Id = 1,
Name = "Phone"
};
Concise (positional) form of C# Records
C# also provides a shortened form of record declaration:
public record Product(int Id, string Name, decimal Price);
var product = new Product(1, "PC", 1000.00m);
A single line of code! We assign all the properties using a record's constructor which is called a primary constructor.
Under the hood this code is translated to a classic form of a record declaration with all properties being init only.
This form of record is also called a positional record as all properties under the hood are created in the exact order as their position in the primary constructor.
Positional form of declaration looks really elegant and concise when a record doesn't have a lot of fields.
Records immutability
Records in C# are immutable by their nature, you can't change properties of record object.
Instead you should use the when keyword to create a new instance of record with updated properties:
var product = new Product(1, "PC", 1000.00m);
// This code doesn't compile: can't change a record property
product.Price = 1200.00m;
// Instead create a new product with updated price
var product2 = product with
{
Price = 1200.00m
};
Records equality comparison
A default equality comparison behaviour for different C# type is as follows:
- struct: two objects are equal if they have the same type and store the same values.
- class: two objects are equal if their references in memory are equal.
- record: two objects are equal if they have the same type and store the same values.
Records are reference types but have a value object behaviour when it comes to equality comparison.
var product1 = new Product(1, "PC", 1000.00m);
var product2 = new Product(2, "Laptop", 1500.00m);
var product3 = new Product(1, "PC", 1000.00m);
Console.WriteLine(product1 == product2); // prints "false"
Console.WriteLine(product1 == product3); // prints "true" as both records have the same values
Records inheritance
As any other class - records support inheritance. Let's have a look on the following example:
public record Product(int Id, string Name, decimal Price);
public record SpecialProduct(int Id, string Name, decimal Price, decimal Discount)
: Product(Id, Name, Price);
var product = new Product(1, "PC", 1000.00m);
var specialProduct = new SpecialProduct(1, "PC", 1000.00m, 100.00m);
Primary constructors are utilized here to pass properties to a parent record.
When using a classic form of declaration, records look the same as classes:
public record SpecialProduct : Product
{
public SpecialProduct(int id, string name, decimal price, decimal discount)
: base(id, name, price)
{
Discount = discount;
}
public decimal Discount { get; init; }
}
Record structs
In C# 10 a records structs were introduced:
public readonly record struct Point(double X, double Y);
public readonly record struct Point
{
public double X { get; init; }
public double Y { get; init; }
}
public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
}
Records structs can be positional structs with primary constructor, have a classic form of declaration and even be declared as readonly record struct.
Records structs have the same behaviour as regular records but are a value objects.
NOTE: A classic record could be also declared using record class phrase but this is usually shortened to just a record.
Record's ToString Method
Records out of the box provide a useful implementation of the ToString method:
var product1 = new Product(1, "PC", 1000.00m);
Console.WriteLine(product1); // Product { Id = 1, Name = PC, Price = 1000,00 }
ToString method prints all of values of the record properties which is extremely useful for logging and debugging.
When To Use Records
Records in C# are a powerful feature and are preferred to be used in the following scenarios:
- Data Transfer Objects (DTOs): Records are an excellent choice for DTOs in API development due to their immutable nature. They ensure that data objects can't be accidentally modified in any layer of an application.
- REPR (Request-Endpoint-Response) pattern: Records are an excellent choice for request and response models that should not be modified after creation.
- Value objects in DDD: In Domain-Driven Design, records can be used for creation of value objects where immutability and value-based equality are essential.
- Read-Only Data: Records is a preferred choice in any scenarios that require immutable (read-only) data.
While records are a great and useful feature, there are scenarios where they shouldn't be used:
- Entity Framework Entities: Entity Framework relies heavily on change tracking for database operations and utilizes a reference equality comparison. That's why records with their value-based equality comparison are a bad choice here.
- UI Data Binding: For applications with UI data binding that requires frequent updates (like in WPF or Blazor), the immutability of records can be limiting. Mutable POCO (Plain Old CLR Objects) are typically more suitable in these cases.
- Performance-Critical Sections: If you're working in a performance-critical part of your application, using records where object mutations are frequent - can lead to performance overhead due to a big number of created new instances of records on every modification.
Hope you find this blog post useful. Happy coding!
Originally published at https://antondevtips.com.
After reading the post consider the following:
- Subscribe to receive newsletters with the latest blog posts
- Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)
If you like my content — consider supporting me
Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!
Posted on April 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.