This content originally appeared on DEV Community and was authored by mohamed Tayel
Records in C# are a game-changer when it comes to defining data-centric types. They offer built-in features like value-based equality, immutability, and a concise syntax. While the primary constructor is great for defining essential properties, you might need to extend records with optional properties, methods, or derived values for more complex scenarios.
In this article, we’ll explore how to enhance records by adding additional members and discuss scenarios where they shine. We’ll include clear examples and practical assignments to deepen your understanding.
Why Extend a Record?
The primary constructor in a record defines the minimum information needed to create an instance. However, there are cases where you might need:
- Derived Properties: Compute values based on existing properties.
- Optional Fields: Handle data that isn’t always required during initialization.
- Custom Methods: Add functionality specific to the record’s purpose.
C# allows records to be flexible by defining a body where you can add fields, properties, and methods.
Adding Derived Properties
Derived properties are useful when you need to compute values based on other properties. Let’s define a Book record with Title and Author as required fields. We’ll also add a computed property, DisplayTitle, that combines them for display purposes.
public record Book(string Title, string Author)
{
// Derived property
public string DisplayTitle => $"{Title} by {Author}";
}
Usage Example:
var book = new Book("The Great Gatsby", "F. Scott Fitzgerald");
Console.WriteLine(book.DisplayTitle); // Output: The Great Gatsby by F. Scott Fitzgerald
Adding Optional Properties with init
Optional properties aren’t always a part of the primary constructor. Using the init keyword, you can define properties that can only be set during initialization and remain immutable afterward.
Here’s an example with a Car record:
public record Car(string Make, string Model)
{
// Optional immutable property
public int? Year { get; init; }
}
Usage Example:
var car = new Car("Tesla", "Model S") { Year = 2021 };
Console.WriteLine($"{car.Make} {car.Model}, Year: {car.Year}");
// Output: Tesla Model S, Year: 2021
// Attempting to modify 'Year' after initialization will cause a compiler error:
// car.Year = 2022; // Error
The init keyword ensures that optional properties are immutable after being initialized.
Using Nested Records for Complex Data
Nested records are a powerful way to handle complex data. Let’s define a Customer record with a nested Address record:
public record Address(string Street, string City);
public record Customer(string Name)
{
// Optional property with a nested record
public Address? Address { get; init; }
}
Usage Example:
var customer = new Customer("Alice")
{
Address = new Address("123 Elm Street", "Springfield")
};
Console.WriteLine($"{customer.Name} lives at {customer.Address?.Street}, {customer.Address?.City}");
// Output: Alice lives at 123 Elm Street, Springfield
The nested Address record integrates seamlessly, allowing for clean and concise data structures.
Adding Custom Methods
Custom methods let you add specific functionality to records. For instance, let’s add a method to a Transaction record to calculate the total cost:
public record Transaction(string Item, int Quantity, decimal UnitPrice)
{
// Method to calculate the total cost
public decimal CalculateTotal() => Quantity * UnitPrice;
}
Usage Example:
var transaction = new Transaction("Laptop", 2, 1500.00m);
Console.WriteLine(transaction.CalculateTotal()); // Output: 3000.00
This method encapsulates behavior directly within the record, keeping the logic close to the data.
Immutability with with Expressions
While records are immutable, you can create new instances with modified properties using the with expression. This is particularly useful when you want to update data while preserving the original object.
Let’s enhance the Customer record with this feature:
var originalCustomer = new Customer("Alice")
{
Address = new Address("123 Elm Street", "Springfield")
};
var updatedCustomer = originalCustomer with { Address = new Address("456 Oak Avenue", "Shelbyville") };
Console.WriteLine(originalCustomer.Address?.Street); // Output: 123 Elm Street
Console.WriteLine(updatedCustomer.Address?.Street); // Output: 456 Oak Avenue
The with expression ensures immutability while allowing flexibility for updates.
Assignments to Test Your Understanding
Easy Level
- Create a
Movierecord with:-
TitleandDirectoras primary constructor properties. - A computed property
Descriptionthat combines both.
-
Example Output:
var movie = new Movie("Inception", "Christopher Nolan");
Console.WriteLine(movie.Description); // Output: Inception by Christopher Nolan
- Add an optional property
Ratingusinginitto theMovierecord.
Medium Level
- Define a
Productrecord with:-
NameandPricein the primary constructor. - A method
ApplyDiscount(decimal percentage)that returns a newProductwith the discounted price.
-
Example Output:
var product = new Product("Laptop", 2000.00m);
var discountedProduct = product.ApplyDiscount(10);
Console.WriteLine(discountedProduct.Price); // Output: 1800.00
- Add a nested record
Categoryto represent the product category, and integrate it into theProductrecord.
Difficult Level
- Create an
Orderrecord:- Include
OrderId,ItemName,Quantity, andUnitPricein the primary constructor. - Add a computed property
TotalPricethat calculates the total. - Add a method
UpdateQuantity(int newQuantity)that returns a newOrderwith the updated quantity.
- Include
Example Output:
var order = new Order("ORD001", "Tablet", 2, 500.00m);
Console.WriteLine(order.TotalPrice); // Output: 1000.00
var updatedOrder = order.UpdateQuantity(5);
Console.WriteLine(updatedOrder.TotalPrice); // Output: 2500.00
When to Use Records vs. Classes
-
Use records for:
- Immutable data-centric types.
- Scenarios requiring value-based equality.
- Clean and compact definitions.
-
Use classes for:
- Types with significant behavior or mutable state.
- Complex hierarchies and relationships.
Conclusion
Extending records with additional members, computed properties, and methods allows you to create versatile and maintainable data models. By leveraging features like the init keyword and with expressions, you can balance immutability and flexibility.
This content originally appeared on DEV Community and was authored by mohamed Tayel