This content originally appeared on DEV Community and was authored by mohamed Tayel
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
initfor optional properties you want to set during initialization only. - Properties defined with
initremain immutable after the object is created. - Combine
initwithwithexpressions 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
Personrecord with the following properties:-
FirstName(string) -
LastName(string) - Add a computed property
FullNamethat combinesFirstNameandLastName.
-
Expected Output:
var person = new Person("John", "Doe");
Console.WriteLine(person.FullName); // Output: John Doe
- Add an optional
Ageproperty usinginitto thePersonrecord, 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
Productrecord with:- A primary constructor for
Name(string) andPrice(decimal). - Add a method
ApplyDiscount(decimal percentage)that returns a newProductinstance with the discounted price using thewithexpression.
- 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
Categoryproperty to theProductrecord 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
Customerrecord with:- A primary constructor for
Name(string). - An
Addressrecord nested within theCustomerrecord. TheAddressrecord should have propertiesStreetandCity. - Add a
MoveToNewAddress(Address newAddress)method in theCustomerrecord that returns a newCustomerwith the updatedAddressusing thewithexpression.
- 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
Orderrecord:- A primary constructor for
OrderId(string),Product(string), andQuantity(int). - Add a computed property
TotalPricethat calculates the total price given a fixedPricePerUnit(e.g., 20.00 per unit). - Add a method
UpdateQuantity(int newQuantity)to return a newOrderinstance 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
withexpression to create new instances of records with modified values. - Use the
initkeyword to ensure optional properties are immutable after initialization. - Leverage computed properties to derive values dynamically based on existing data.
This content originally appeared on DEV Community and was authored by mohamed Tayel