A Guide To Crafting Immutable Objects With C# 10's Init-Only Properties

Overview

Every new version of C# brings new features that enhance the language's expressiveness and productivity. C# 10 introduced init-only properties, which revolutionized the creation of immutable objects. In this detailed exploration, we'll delve into the concept of init-only properties and how they revolutionize the world of coding.

Immutable Objects and the Need for Initialization

Immutable objects, once created, remain unmodifiable. Creating immutable objects with traditional C# required writing verbose constructors or using object initializers, but they offered many advantages, including thread safety, enhanced security, and simplified code reasoning. Streamlining the initialization process of immutable objects with init-only properties in C# 10 solves this problem elegantly.

Defining Init-Only Properties

Take a classic Person class with FirstName, LastName, DateOfBirth, Email, Phone, Address and Orders properties as an example below:

//The File name is Person.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;
public class Person
{
    public string FirstName { get; }
    public string LastName { get; }
    public DateTime DateOfBirth { get; }
    public string Email { get; }
    public string Phone { get; }
    public Address Address { get; }
    public List<Order> Orders { get; }

    public Person(string firstName, string lastName, DateTime dateOfBirth, string email, string phone, Address address, List<Order> orders)
    {
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        Email = email;
        Phone = phone;
        Address = address;
        Orders = orders;
    }
}
//The File name is public class Address.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;
public class Address
{
    public Guid AddressID { get; }
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string ZipCode { get; }

    public Address(Guid addressID, string street, string city, string state, string zipCode)
    {
        AddressID = addressID;
        Street = street;
        City = city;
        State = state;
        ZipCode = zipCode;
    }
}
//The File name is public class Order.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;
public class Order
{
    public Guid OrderId { get; }
    public DateTime OrderDate { get; }
    public string ProductName { get; }
    public int Quantity { get; }
    public decimal Price { get; }

    public Order(Guid orderId, DateTime orderDate, string productName, int quantity, decimal price)
    {
        OrderId = orderId;
        OrderDate = orderDate;
        ProductName = productName;
        Quantity = quantity;
        Price = price;
    }
}

The conventional way to initialize FirstName, LastName, DateOfBirth, Email, Phone, Address and Orders properties is to define a constructor. C# 10's init-only properties simplify this process significantly:

//The File name is Person.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;
public class Person
{
    public string FirstName { get; init; }
    public string LastName { get;  init;}
    public DateTime DateOfBirth { get; init; }
    public string Email { get; init; }
    public string Phone { get; init; }
    public Address Address { get; init; }
    public List<Order> Orders { get; init; }

    public Person(string firstName, string lastName, DateTime dateOfBirth, string email, string phone, Address address, List<Order> orders)
    {
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        Email = email;
        Phone = phone;
        Address = address;
        Orders = orders;
    }
}

Init specifies that these properties are read-only once the object is created, ensuring their read-only status.

Streamlined Initialization

With init-only properties, creating and initializing instances of immutable objects becomes concise and expressive.

using CSharp10_ImmutableObjects.Models;

Console.WriteLine("Hello, from Ziggy Rafiq!");

var address = new Address(Guid.NewGuid(), "123 Main St", "Anytown", "State", "12345");

var orders = new List<Order>
{
    new Order(Guid.NewGuid(), DateTime.Now, "Product1", 2, 20.00m),
    new Order(Guid.NewGuid(), DateTime.Now, "Product2", 1, 15.00m)
};

var person = new Person(
     
        
    firstName:"Lisa",
    lastName: "Smith",
    dateOfBirth:new DateTime(1980, 10, 10),
    email: "[email protected]",
    phone: "123-456-7890",
    address: address,
    orders: orders
);
Console.WriteLine(person.ToString());

It offers a clean and readable way to initialize properties during object creation. Modifying these properties once instantiated will result in a compilation error, enforcing immutability.

Benefits of Init-Only Properties

  • Immutability by Design: Init-only properties facilitate the creation of immutable objects, enhancing the predictability and maintainability of code.
  • Readability and Conciseness: Using init-only properties to initialize an object leads to more readable and expressive code.
  • Compiler-Enforced Immutability: Compilers prevent accidental modification of init-only properties by ensuring they can only be set during object initialization.

Using Init-Only Properties in a Real-World Scenario

As an extension of our Person example, let's consider an Address class with only init properties:

//The File name is public class Address.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;
public class Address
{
    public Guid AddressID { get; init; }
    public string Street { get; init; }
    public string City { get; init; }
    public string State { get; init; }
    public string ZipCode { get; init; }

    public Address(Guid addressID, string street, string city, string state, string zipCode)
    {
        AddressID = addressID;
        Street = street;
        City = city;
        State = state;
        ZipCode = zipCode;
    }
}

The initialization of an Address object is straightforward:

var address = new Address(Guid.NewGuid(), "123 Main St", "Anytown", "State", "12345");

Additional Considerations
 

Compatibility and Adoption

Though init-only properties were introduced in C# 10, they have been enhanced and optimized in C# 10. As a result, developers should make sure they are using a compatible compiler and runtime environment to take full advantage of init-only properties.

//The File name is Person.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;
public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public DateTime DateOfBirth { get; init; }
    public string Email { get; init; }
    public string Phone { get; init; }
    public Address Address { get; init; }
    public List<Order> Orders { get; init; }

    public Person(string firstName, string lastName, DateTime dateOfBirth, string email, string phone, Address address, List<Order> orders)
    {
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        Email = email;
        Phone = phone;
        Address = address;
        Orders = orders;
    }

    public override string ToString()
    {
        return $"{FirstName} {LastName}";
    }

}
using CSharp10_ImmutableObjects.Models;

Console.WriteLine("Hello, from Ziggy Rafiq!");

var address = new Address(Guid.NewGuid(), "123 Main St", "Anytown", "State", "12345");

var orders = new List<Order>
{
    new Order(Guid.NewGuid(), DateTime.Now, "Product1", 2, 20.00m),
    new Order(Guid.NewGuid(), DateTime.Now, "Product2", 1, 15.00m)
};

var person = new Person(
     
        
    firstName:"Lisa",
    lastName: "Smith",
    dateOfBirth:new DateTime(1980, 10, 10),
    email: "[email protected]",
    phone: "123-456-7890",
    address: address,
    orders: orders
);
Console.WriteLine(person.ToString());

Console.WriteLine($"Person: {person}");



// Check the C# version being used
Console.WriteLine($"C# Version: {Environment.Version}");

// Check for C# 10 compatibility
if (Environment.Version.Major >= 6) // C# 10 corresponds to .NET 6
{
    Console.WriteLine("Init-only properties are supported!");
}
else
{
    Console.WriteLine("Init-only properties may not be fully supported. Consider upgrading your C# compiler or runtime environment.");
}

In the code example above we define a Person class with init-only properties for FirstName and LastName. The object is created using these properties and then the C# version is checked to determine if init-only properties are supported. The C# version must be at least 6 (corresponding to .NET 6, when C# 10 was introduced), indicating that init-only properties are supported. For full compatibility, it recommends updating the C# compiler or runtime environment.

Performance Considerations

A developer should be aware of the performance implications of init-only properties, even though they can improve code readability and maintainability. Using init-only properties to initialize large numbers of objects may incur overhead, especially if the initialization logic is complex. When object creation and initialization are critical to performance, performance profiling and optimization may be necessary.


const int numberOfIterations = 1000000;

Stopwatch stopwatch = new Stopwatch();

// Measure performance with traditional properties
stopwatch.Start();
for (int i = 0; i < numberOfIterations; i++)
{
    var person2 = new Person(


     firstName: "Lisa",
     lastName: "Smith",
     dateOfBirth: new DateTime(1980, 10, 10),
     email: "[email protected]",
     phone: "123-456-7890",
     address: address,
     orders: orders
 );
    // Perform some operations with the person object
}
stopwatch.Stop();
Console.WriteLine($"Time taken with traditional properties: {stopwatch.ElapsedMilliseconds} ms");

// Measure performance with init-only properties
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < numberOfIterations; i++)
{
    var person3 = new Person(


     firstName: "Lisa",
     lastName: "Smith",
     dateOfBirth: new DateTime(1980, 10, 10),
     email: "[email protected]",
     phone: "123-456-7890",
     address: address,
     orders: orders
 );
    // Perform some operations with the person object
}
stopwatch.Stop();
Console.WriteLine($"Time taken with init-only properties: {stopwatch.ElapsedMilliseconds} ms");

Using both traditional properties and init-only properties, we compare the performance of creating and initializing Person objects. Using each approach, we measure the time it takes to instantiate and operate many objects. By doing so, we can better understand the potential performance overhead associated with using init-only properties, especially when initializing many objects at once. Developers can use performance profiling tools like Stopwatch to identify and optimize performance-critical sections of their codebase.

Documentation and Best Practices

Documenting init-only properties in your codebase and establishing best practices for their adoption is essential as with any language feature. Providing guidelines on when and how to use init-only properties, along with examples and code samples, can ensure your development team uses them consistently and effectively.

//The File name is Person.cs inside Models folder
namespace CSharp10_ImmutableObjects.Models;

/// <summary>
/// Represents a Person with read-only properties for first and last name.
/// </summary>
public class Person
{
    /// <summary>
    /// Gets the first name of the person.
    /// </summary>
    public string FirstName { get; init; }

    /// <summary>
    /// Gets the last name of the person.
    /// </summary>
    public string LastName { get; init; }

    /// <summary>
    /// Gets the date of birth of the person.
    /// </summary>
    public DateTime DateOfBirth { get; init; }

    /// <summary>
    /// Gets the email of the person.
    /// </summary>
    public string Email { get; init; }

    /// <summary>
    /// Gets the phone number of the person.
    /// </summary>
    public string Phone { get; init; }

    /// <summary>
    /// Gets the address of the person.
    /// </summary>
    public Address Address { get; init; }

    /// <summary>
    /// Gets the orders of the person.
    /// </summary>
    public List<Order> Orders { get; init; }

    /// <summary>
    /// Initializes a new instance of the <see cref="Person"/> class with the specified first and last name.
    /// </summary>
    /// <param name="firstName">The first name of the person.</param>
    /// <param name="lastName">The last name of the person.</param>
    /// <param name="dateOfBirth">The date of birth of the person.</param>
    /// <param name="email">The email of the person.</param>
    /// <param name="phone">The phone number of the person.</param>
    /// <param name="address">The address of the person.</param>
    /// <param name="orders">The orders of the person.</param>
    public Person(string firstName, string lastName, DateTime dateOfBirth, string email, string phone, Address address, List<Order> orders)
    {
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        Email = email;
        Phone = phone;
        Address = address;
        Orders = orders;
    }

    /// <summary>
    /// Person Name is returned
    /// </summary>
    /// <returns>First Name and Last Name returned</returns>
    public override string ToString()
    {
        return $"{FirstName} {LastName}";
    }

}

In the code example above, the Person class is documented with XML comments to explain the purpose of each property and constructor. Additionally, best practices for using init-only properties are provided in the Main method of the Program class, emphasizing clarity, consistency, and documentation. As a result, developers will be able to use init-only properties in their codebase effectively and consistently.

Summary

C# 10 introduces init-only properties, offering a powerful and graceful means of crafting immutable objects. In addition to simplifying initialization, these properties also enhance code cleanliness, readability, and resilience, facilitating easier maintenance. By embracing immutability in your C# projects through init-only properties, you can ensure robustness and simplify comprehension and maintenance. This advancement signifies a significant step forward in the language's support for immutable objects, streamlining initialization and boosting code integrity. By using init-only properties, developers can develop software with greater predictability, reliability, and manageability, aligned with evolving standards for precision and confidence in C# development.

Please do not forget to like this article if you have found it useful and follow me on my LinkedIn https://www.linkedin.com/in/ziggyrafiq/ also I have uploaded the source code for this article on my GitHub Repo    https://github.com/ziggyrafiq/CSharp10-ImmutableObjects


Similar Articles
Capgemini
Capgemini is a global leader in consulting, technology services, and digital transformation.