DEV Community

mohamed Tayel
mohamed Tayel

Posted on

c# advanced: Enhancing Records Adding Flexibility with Additional Members

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}";
}
Enter fullscreen mode Exit fullscreen mode

Usage Example:

var book = new Book("The Great Gatsby", "F. Scott Fitzgerald");
Console.WriteLine(book.DisplayTitle); // Output: The Great Gatsby by F. Scott Fitzgerald
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Usage Example:

var transaction = new Transaction("Laptop", 2, 1500.00m);
Console.WriteLine(transaction.CalculateTotal()); // Output: 3000.00
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The with expression ensures immutability while allowing flexibility for updates.


Assignments to Test Your Understanding

Easy Level

  1. Create a Movie record with:
    • Title and Director as primary constructor properties.
    • A computed property Description that combines both.

Example Output:

   var movie = new Movie("Inception", "Christopher Nolan");
   Console.WriteLine(movie.Description); // Output: Inception by Christopher Nolan
Enter fullscreen mode Exit fullscreen mode
  1. Add an optional property Rating using init to the Movie record.

Medium Level

  1. Define a Product record with:
    • Name and Price in the primary constructor.
    • A method ApplyDiscount(decimal percentage) that returns a new Product with the discounted price.

Example Output:

   var product = new Product("Laptop", 2000.00m);
   var discountedProduct = product.ApplyDiscount(10);
   Console.WriteLine(discountedProduct.Price); // Output: 1800.00
Enter fullscreen mode Exit fullscreen mode
  1. Add a nested record Category to represent the product category, and integrate it into the Product record.

Difficult Level

  1. Create an Order record:
    • Include OrderId, ItemName, Quantity, and UnitPrice in the primary constructor.
    • Add a computed property TotalPrice that calculates the total.
    • Add a method UpdateQuantity(int newQuantity) that returns a new Order with the updated quantity.

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)