C# A to Z: Assignment with Init-Only Setters

Shahed Chowdhuri @ Microsoft - Jan 4 '21 - - Dev Community

Update: Due to new personal commitments and more work commitments in 2021, I wasn't able to make much progress with my weekly C# A-Z series.

For now, I'll focus on some new content for my regular blog (WakeUpAndCode.com) and hope to revisit the A-Z series with .NET 6.

Original Post:

Introduction

This is the first of a new series of posts on C# topics. In this series, we’ll cover 26 topics in 2021, titled C# A-Z! This series will cover mostly C# 9 topics with .NET 5, plus some additional language features that have been around with earlier releases of C#.

If you're not familiar with top-level programs in C# 9, make sure you check out the Prelude post for this series:

Let's kick things off with the letter A!

  • Assignment with Init-Only Setters

Source code:

Prerequisites

Before you get started, please install the latest version of .NET and your preferred IDE or code editor, e.g. Visual Studio 2019 or Visual Studio Code.

Without Init-Only Setters

Init-only setters were introduced with C# 9, allowing developers to create immutable properties in C# classes. Before we get into the new feature, let's explore how your code would look without this feature.

The PatientInfo class in the >NET Core 3.1 project contains a set of properties for a hospital patient, as shown below:

namespace assign_netcore31.Models
{
    public class PatientInfo
    {
        public int Id { get; set; }
        public string LastName { get; set; }
        public string FirstName { get; set; }
        public bool IsWell { get; set; }
    }
}

Enter fullscreen mode Exit fullscreen mode

Each of these properties have a getter and setter, and can be initialized in a Program that uses this class.

using assign_netcore31.Models;
using System;

namespace assign_netcore31
{
    class Program
    {
        static void Main(string[] args)
        {
            // ...
            // create a new patientInfo object
            var patientInfo = new PatientInfo
            {
                Id = 12345,
                LastName = "Doe",
                FirstName = "John",
                IsWell = true
            };

            Console.WriteLine($"ID={patientInfo.Id}");
            Console.WriteLine($"Full Name={patientInfo.FirstName} {patientInfo.LastName}");
            Console.WriteLine($"Sick? {!patientInfo.IsWell}");

            // ...
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

If you run the program from the sample project, you should see the following output. Note that you can easily re-assign fields that have already been initialized.

Hello World from .NET Core 3.1!
ID=12345
Full Name=John Doe
Sick? False
New Id=99999
New last name=Smith3000
Enter fullscreen mode Exit fullscreen mode

Let's say we want to make the Id field "immutable", i.e. prevent it from being changed once it has been initialized. We could remove the setter from its property to prevent intialization. We would also have to create a constructor to pass in an Id value for initial assignment.

The following class PatientInfoWithId, illustrates how this can be achieved.

namespace assign_netcore31.Models
{
    public class PatientInfoWithId
    {
        public int Id { get; }
        public string LastName { get; set; }
        public string FirstName { get; set; }
        public bool IsWell { get; set; }

        public PatientInfoWithId(int id)
        {
            Id = id;
        }

        public PatientInfoWithId(int id, string lastName, string firstName, bool isWell)
        {
            Id = id;
            LastName = lastName;
            FirstName = firstName;
            IsWell = isWell;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

The following Program makes use of the above class, to initialize a patient object with an Id value specified in the constructor.

using assign_netcore31.Models;
using System;

namespace assign_netcore31
{
    class Program
    {
        static void Main(string[] args)
        {
            // ...

            // create a new patientInfoWithId object
            var patientInfoWithId = new PatientInfoWithId(67890);
            patientInfoWithId.LastName = "Smith";
            patientInfoWithId.FirstName = "Joe";
            patientInfoWithId.IsWell = false;

            Console.WriteLine($"ID={patientInfoWithId.Id}");
            Console.WriteLine($"Full Name={patientInfoWithId.FirstName} {patientInfoWithId.LastName}");
            Console.WriteLine($"Sick? {!patientInfoWithId.IsWell}");
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

If you re-run the program from the 3.1 sample project (assign-netcore31), you should also see the following output.

...
ID=67890
Full Name=Joe Smith
Sick? True
Enter fullscreen mode Exit fullscreen mode

By uncommenting the commented line of code provided, you could attempt to reassign the Id value.

            // UNCOMMENT BELOW to change Id: doesn't work 
            //patientInfoWithId.Id = 99999;
            Console.WriteLine($"New Id={patientInfoWithId.Id}");
        }
Enter fullscreen mode Exit fullscreen mode

However, the Id value cannot be reassigned in this way, since there's no setter for the property. To achieve the same thing with fewer lines of code, let's jump into the .NET 5 project using C# 9.

Using Init-Only Setters

In the .NET 5 sample project, the PatientInfo class also contains a similar set of properties for a hospital patient, as shown below:

namespace assign_net5.Models
{
    public class PatientInfo
    {
        public int Id { get; init; }
        public string LastName { get; init; }
        public string FirstName { get; set; }
        public bool IsWell { get; set; }

    }
}
Enter fullscreen mode Exit fullscreen mode

The big difference is the init keyword used for the Id and LastName fields:

        public int Id { get; init; }
        public string LastName { get; init; }
Enter fullscreen mode Exit fullscreen mode

This allows us to use this class in a simpler way in the .NET 5 program.

using assign_net5.Models;
using System;

Console.WriteLine("Hello World from .NET 5!");

var patientInfo = new PatientInfo
{
    Id = 12345,
    LastName = "Doe",
    FirstName = "John",
    IsWell = true
};

Console.WriteLine($"ID={patientInfo.Id}");
Console.WriteLine($"Full Name={patientInfo.FirstName} {patientInfo.LastName}");
Console.WriteLine($"Sick? {!patientInfo.IsWell}");
Enter fullscreen mode Exit fullscreen mode

This should result in the following output:

Hello World from .NET 5!
ID=12345
Full Name=John Doe
Sick? False
New Id=12345
New last name=Doe
Enter fullscreen mode Exit fullscreen mode

Note that we were easily able to assign the property values without the need for any explicit constructor. Better yet, the init keyword prevents us from trying to reassign the Id or LastName properties.

If you try commenting out the remaining code provided, the code won't compile (as intended).

// UNCOMMENT BELOW to change Id: doesn't work with C#9 in .NET 5
//patientInfo.Id = 99999;
Console.WriteLine($"New Id={patientInfo.Id}");

// UNCOMMENT BELOW to change last name: doesn't work with C#9 in .NET 5
// patientInfo.LastName = "Smith3000";
Console.WriteLine($"New last name={patientInfo.LastName}");  
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this blog post, we have covered the super-simple init acccessor in C# 9 using .NET 5. We spent most of our time exploring a .NET Core 3.1 version of a similar project, which resulted in more code. This helped to illustrate the simplicity and efficiency of the init keyword, when used to create immutable C# properties.

What's Next

This blog post is the first in the C# A-Z series in 2021. Stay tuned for a total of 26 different topics in the weeks and months ahead!

Up next:

  • A is for: Assignment with Init-Only Setters
  • B is for: Bits in Native-Sized Integers
  • C is for: TBA
  • D is for: TBA
  • E is for: TBA
  • F is for: TBA
  • G is for: TBA
  • H is for: TBA
  • I is for: TBA
  • J is for: TBA
  • K is for: TBA
  • L is for: TBA
  • M is for: TBA
  • N is for: TBA
  • O is for: TBA
  • P is for: TBA
  • Q is for: TBA
  • R is for: TBA
  • S is for: TBA
  • T is for: TBA
  • U is for: TBA
  • V is for: TBA
  • W is for: TBA
  • X is for: TBA
  • Y is for: TBA
  • Z is for: TBA

References

All future code samples will be added to the existing GitHub repository:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .