DEV Community

mohamed Tayel
mohamed Tayel

Posted on

c# advanced: Adding Additional Members to a Record in C#

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

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

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

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

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

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

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 with with 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

  1. Create a Person record with the following properties:
    • FirstName (string)
    • LastName (string)
    • Add a computed property FullName that combines FirstName and LastName.

Expected Output:

   var person = new Person("John", "Doe");
   Console.WriteLine(person.FullName); // Output: John Doe
Enter fullscreen mode Exit fullscreen mode
  1. Add an optional Age property using init to the Person record, and initialize it while creating an instance.

Expected Output:

   var person = new Person("Jane", "Smith") { Age = 30 };
   Console.WriteLine(person.Age); // Output: 30
Enter fullscreen mode Exit fullscreen mode

Medium Level

  1. Create a Product record with:
    • A primary constructor for Name (string) and Price (decimal).
    • Add a method ApplyDiscount(decimal percentage) that returns a new Product instance with the discounted price using the with expression.

Expected Output:

   var product = new Product("Laptop", 1500.00m);
   var discountedProduct = product.ApplyDiscount(10); // 10% discount
   Console.WriteLine(discountedProduct.Price); // Output: 1350.00
Enter fullscreen mode Exit fullscreen mode
  1. Add an optional Category property to the Product record using init. Ensure this property is immutable after initialization.

Expected Output:

   var product = new Product("Tablet", 800.00m) { Category = "Electronics" };
   Console.WriteLine(product.Category); // Output: Electronics
Enter fullscreen mode Exit fullscreen mode

Difficult Level

  1. Create a Customer record with:
    • A primary constructor for Name (string).
    • An Address record nested within the Customer record. The Address record should have properties Street and City.
    • Add a MoveToNewAddress(Address newAddress) method in the Customer record that returns a new Customer with the updated Address using the with expression.

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
Enter fullscreen mode Exit fullscreen mode
  1. Create an immutable Order record:
    • A primary constructor for OrderId (string), Product (string), and Quantity (int).
    • Add a computed property TotalPrice that calculates the total price given a fixed PricePerUnit (e.g., 20.00 per unit).
    • Add a method UpdateQuantity(int newQuantity) to return a new Order instance with the updated quantity and recalculated TotalPrice.

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

Hints for Completion

  1. Use the with expression to create new instances of records with modified values.
  2. Use the init keyword to ensure optional properties are immutable after initialization.
  3. Leverage computed properties to derive values dynamically based on existing data.

Top comments (1)

Collapse
 
programmerraja profile image
Boopathi • Edited

This article really helped clear up the use of init for optional properties in records. I hadn't realized you could add methods too!