Meta Description
Learn how to enhance records in C# by adding additional members such as properties, methods, and computed values. Discover how to use the init keyword for immutability, value-based equality, and object initializers for clean, flexible record definitions.
Records in C# provide a concise way to represent data with built-in features like value-based equality and immutability. However, the primary constructor may not always cover all use cases. You might need additional properties, methods, or computations for your record. In this article, we’ll explore how to enhance records, including the use of the init
keyword for immutable properties, with clear examples.
Why Add Additional Members?
The primary constructor defines the minimal set of data required to create a record. But what if:
- You need derived values (e.g., full name from first and last names)?
- There’s optional data that doesn’t belong in the primary constructor?
- You want to add methods for specific behaviors?
Records allow you to define a body where you can add fields, properties, and methods, just like in a class.
Example: Adding Optional Properties with init
Sometimes, optional properties are required that aren’t part of the primary constructor. These properties can be made immutable using the init
keyword.
Here’s an example of a Customer
record with an optional Address
property:
public record Address(string Street, string City);
public record Customer(string Name)
{
// Optional immutable property
public Address? Address { get; init; }
}
The init
keyword allows you to set the Address
property only during object initialization.
Usage:
var customer = new Customer("Alice")
{
Address = new Address("123 Main St", "Springfield")
};
// This will cause a compiler error because the `init` property cannot be changed:
// customer.Address = new Address("456 Elm St", "Shelbyville"); // Error
With init
, the property remains immutable after the object is initialized. This ensures thread-safety and consistency.
Example: Combining init
with Computed Properties
Let’s add a FullName
computed property to a Student
record. While the primary constructor covers the essential details, init
is used for an optional property, EnrollmentDate
.
public record Student(string FirstName, string LastName)
{
// Computed property
public string FullName => $"{FirstName} {LastName}";
// Optional immutable property
public DateTime? EnrollmentDate { get; init; }
}
Usage:
var student = new Student("John", "Doe")
{
EnrollmentDate = new DateTime(2023, 9, 1)
};
Console.WriteLine(student.FullName); // Output: John Doe
Console.WriteLine(student.EnrollmentDate); // Output: 9/1/2023 12:00:00 AM
// Attempting to modify EnrollmentDate causes a compiler error:
// student.EnrollmentDate = new DateTime(2024, 1, 1); // Error
This approach ensures that properties like EnrollmentDate
can only be set once during initialization.
What Happens Behind the Scenes with init
The init
keyword creates a setter that can only be called during initialization (e.g., in an object initializer). This ensures immutability after the object is created.
For example, the Student
record above would be equivalent to:
public record Student(string FirstName, string LastName)
{
public string FullName => $"{FirstName} {LastName}";
private DateTime? _enrollmentDate;
public DateTime? EnrollmentDate
{
get => _enrollmentDate;
init => _enrollmentDate = value;
}
}
Example: Updating Immutable Properties with with
While init
properties are immutable after initialization, you can use the with
expression to create a new record instance with modified properties.
Here’s an example:
var originalCustomer = new Customer("Alice")
{
Address = new Address("123 Main St", "Springfield")
};
var updatedCustomer = originalCustomer with { Address = new Address("456 Elm St", "Shelbyville") };
Console.WriteLine(originalCustomer.Address?.Street); // Output: 123 Main St
Console.WriteLine(updatedCustomer.Address?.Street); // Output: 456 Elm St
The with
expression creates a new record instance without modifying the original, ensuring immutability.
Summary of the init
Keyword
- Use
init
for optional properties you want to set during initialization only. - Properties defined with
init
remain immutable after the object is created. - Combine
init
withwith
expressions for safe, immutable updates.
When to Use Records vs. Classes
-
Use records when:
- You need immutable, data-centric types.
- Value-based equality is essential (e.g., domain models or DTOs).
- Compact syntax is desired for clean code.
-
Use classes when:
- Mutability is required.
- Complex behaviors or logic are involved.
Conclusion
The init
keyword is a valuable tool for maintaining immutability in records, especially for optional properties. Combined with other record features, it enables clean, maintainable, and thread-safe code. By enhancing records with additional members and leveraging init
, you can balance flexibility with immutability to create robust data models.
Assignments
Easy Level
- Create a
Person
record with the following properties:-
FirstName
(string) -
LastName
(string) - Add a computed property
FullName
that combinesFirstName
andLastName
.
-
Expected Output:
var person = new Person("John", "Doe");
Console.WriteLine(person.FullName); // Output: John Doe
- Add an optional
Age
property usinginit
to thePerson
record, and initialize it while creating an instance.
Expected Output:
var person = new Person("Jane", "Smith") { Age = 30 };
Console.WriteLine(person.Age); // Output: 30
Medium Level
- Create a
Product
record with:- A primary constructor for
Name
(string) andPrice
(decimal). - Add a method
ApplyDiscount(decimal percentage)
that returns a newProduct
instance with the discounted price using thewith
expression.
- A primary constructor for
Expected Output:
var product = new Product("Laptop", 1500.00m);
var discountedProduct = product.ApplyDiscount(10); // 10% discount
Console.WriteLine(discountedProduct.Price); // Output: 1350.00
- Add an optional
Category
property to theProduct
record usinginit
. Ensure this property is immutable after initialization.
Expected Output:
var product = new Product("Tablet", 800.00m) { Category = "Electronics" };
Console.WriteLine(product.Category); // Output: Electronics
Difficult Level
- Create a
Customer
record with:- A primary constructor for
Name
(string). - An
Address
record nested within theCustomer
record. TheAddress
record should have propertiesStreet
andCity
. - Add a
MoveToNewAddress(Address newAddress)
method in theCustomer
record that returns a newCustomer
with the updatedAddress
using thewith
expression.
- A primary constructor for
Expected Output:
var customer = new Customer("Alice") { Address = new Address("123 Elm St", "Springfield") };
var movedCustomer = customer.MoveToNewAddress(new Address("456 Oak St", "Shelbyville"));
Console.WriteLine(customer.Address.Street); // Output: 123 Elm St
Console.WriteLine(movedCustomer.Address.Street); // Output: 456 Oak St
- Create an immutable
Order
record:- A primary constructor for
OrderId
(string),Product
(string), andQuantity
(int). - Add a computed property
TotalPrice
that calculates the total price given a fixedPricePerUnit
(e.g., 20.00 per unit). - Add a method
UpdateQuantity(int newQuantity)
to return a newOrder
instance with the updated quantity and recalculatedTotalPrice
.
- A primary constructor for
Expected Output:
var order = new Order("ORD001", "Smartphone", 2);
Console.WriteLine(order.TotalPrice); // Output: 40.00
var updatedOrder = order.UpdateQuantity(5);
Console.WriteLine(updatedOrder.TotalPrice); // Output: 100.00
Hints for Completion
- Use the
with
expression to create new instances of records with modified values. - Use the
init
keyword to ensure optional properties are immutable after initialization. - Leverage computed properties to derive values dynamically based on existing data.
Top comments (1)
This article really helped clear up the use of
init
for optional properties in records. I hadn't realized you could add methods too!