ICYMI C# 9 New Features: Reduce Boilerplate Constructor Code with Init Only Property Setters

This is part of a series of articles on new features introduced in C# 9.

Prior to C# 9 if you wanted to create properties that can be set only when the object is created, you could make  the property setter private and use constructor arguments to set them:

class PaymentV1
{
    public Guid Id { get; private set; }
    public decimal Value { get; private set; }
    public string Notes { get; set; }
    public PaymentV1(Guid id, decimal value)
    {
        Id = id;
        Value = value;
    }
}

In the preceding code, the Id and Value properties can only be set when the object is created by supplying constructor parameters. Once the Payment has been created you can’t set the Id or Value properties from outside the object instance.

In the preceding code, we have to add the extra constructor code just to create the “externally immutable” properties (they can still be set from code inside the class).

C# 9 introduces the concept of init only setters. These allow you to create immutable properties without needing to write the extra constructor boilerplate code:

class PaymentV2
{
    public Guid Id { get; init; }
    public decimal Value { get; init; }
    public string Notes { get; set; }
}

Notice in the preceding code that the Id and Value properties now use the init keyword instead of the set keyword. Also notice that we no longer need to create a constructor.

To set these immutable properties you need to do so at the time of object construction/creation by using the existing C# property initialization syntax, for example:

var payment2 = new PaymentV2
{
    Id = Guid.NewGuid(),
    Value = 45.50m,
    Notes = "Initial send on Friday."
};

Once this code executes and the payment2 object is created, you will not be able to set Id or Value:

payment2.Id = Guid.NewGuid(); // ERROR - only settable in initializer
payment2.Value = 99.00m; // ERROR - only settable in initializer
payment2.Notes += " Second send on Sunday."; // OK

You can also set init only properties from the constructor of a derived type:

abstract class PaymentBase
{
    protected Guid Id { get; init; }
    protected decimal Value { get; init; }
}

class PaymentV3 : PaymentBase
{
    public string Notes { get; set; }

    public PaymentV3(Guid id, decimal value)
    {
        Id = id;
        Value = value;
    }
}

You could also use init only properties and set a default value if the property is not set at creation and also add validation logic:

class PaymentV4
{
    private readonly string _currencyCode = "USD";

    public string CurrencyCode
    {
        get
        {
            return _currencyCode;
        }

        init 
        {
            if (value is null)
            {
                throw new ArgumentNullException(nameof(CurrencyCode));
            }

            if (value.Length != 3)
            {
                throw new ArgumentOutOfRangeException(nameof(CurrencyCode), "Must be 3 long.");
            }

            // etc.
            _currencyCode = value;
        }
    }
}

With the preceding code, we could try the follow statements:

var payment4 = new PaymentV4(); // Default CurrencyCode of "USD"
var payment4 = new PaymentV4 { CurrencyCode = "AUD" };
var payment4 = new PaymentV4 { CurrencyCode = null }; // Exception
var payment4 = new PaymentV4 { CurrencyCode = "hello" }; // Exception

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Comments (4) -

  • Michael Damatov

    7/16/2021 10:57:37 AM | Reply

    By defining a property "init-only" the property effectively becomes optional because the user gets the option not to set it. "Required" properties still have to be initialized with a constructor.

  • Thomas

    7/16/2021 12:12:45 PM | Reply

    Do you know any refactoring tool that points you toward those possible conversion? I use roselynator, but it does not see those

  • David McClelland

    7/17/2021 2:57:38 PM | Reply

    Have you been able to get this to work with Microsoft's .NET Core dependency injection?  That would save a lot of boilerplate code as well!

  • Joy George Kunjikkuru

    10/11/2021 4:40:50 PM | Reply

    +1 to the question asked by David McClelland
    Below code throws nullref exception as the Opt1 is not initialized
        class MenuService : BackgroundService
        {
            public Option1 Opt1 { get; init; }
            protected async override Task ExecuteAsync(CancellationToken stoppingToken)
            {
                var menu = new Menu()
                    .Add("Menu option 1", async (token) => await Opt1.Execute())
                    .AddSync("Exit", () => Environment.Exit(0));
                await menu.Display(CancellationToken.None);
                await base.StartAsync(stoppingToken);
            }
        }

Add comment

Loading