Object Initialisers and required Members

Set properties in braces as the object is built, lock them with init, and make the compiler demand them with required.

SyntaxTypeName name = new TypeName { Property = value, Other = value };

An object initialiser is a pair of braces after new listing properties and the values to give them. new Invoice { Reference = "INV-1001" } builds the object, then assigns each property in the order written. It is shorthand: the longhand is a new Invoice() line followed by one assignment per property. The result is identical, and the short form is a single expression, so it works as an argument or a return value.

init-only setters

Writing init in place of set declares a setter that may only be used while the object is being built: inside a constructor, or in an object initialiser. Afterwards the property is fixed. That gives you the convenience of named assignments without leaving the object changeable for the rest of its life. init arrived in C# 9.

required members

An initialiser alone does not force anyone to supply a value. The required modifier, added in C# 11, does: any expression that builds the type must assign every required member, and leaving one out is a compile error rather than a surprise default later. required also satisfies the nullable reference types check, so a required string needs no fallback value.

What trips people up

A required member must be at least as visible as its containing type, and its setter must be too. A public class cannot have a required property with a private init, because the caller could not set it.

Example

Example · csharp
// The braces set properties as part of building the object.
Invoice invoice = new Invoice
{
    Reference = "INV-1001",
    Customer = "Acme",
    AmountCents = 4500
};

Console.WriteLine($"{invoice.Reference} {invoice.Customer} {invoice.AmountCents}");

// Reference is init-only, so this would be a compile error:
// invoice.Reference = "INV-1002";

// AmountCents has a normal set accessor, so this is allowed.
invoice.AmountCents = 5000;
Console.WriteLine($"{invoice.Reference} {invoice.Customer} {invoice.AmountCents}");

class Invoice
{
    // required: new Invoice() without these two does not compile.
    // init: settable while building, fixed afterwards.
    public required string Reference { get; init; }
    public required string Customer { get; init; }

    // Not required, and changeable for the object's whole life.
    public int AmountCents { get; set; }
}

// Output:
// INV-1001 Acme 4500
// INV-1001 Acme 5000

When to use it

  • Build a configuration or data object by naming each value, with no constructor taking eight arguments.
  • Make the compiler reject an object that is missing a value instead of finding out at run time.
  • Produce an object that cannot be changed after construction while still writing it readably.

More examples

The initialiser and the longhand it stands for

Seeing the two side by side explains two things at once: why the braces need no commas between the object and the first property, and why the longhand is impossible for an init-only property. Since init permits assignment only during construction, lines like b.Host = ... would not compile against a type that used init, and the initialiser form becomes the only way in.

Example · csharp
// Shorthand: one expression, so it can be returned or passed directly.
Settings a = new Settings { Host = "localhost", Port = 5432 };

// Longhand: the same work. The constructor runs first, then each property
// is assigned in the order you wrote it.
Settings b = new Settings();
b.Host = "localhost";
b.Port = 5432;

// The new() shorthand drops the repeated type name.
Settings c = new() { Host = "localhost", Port = 5432 };

Console.WriteLine($"{a.Host}:{a.Port} {b.Host}:{b.Port} {c.Host}:{c.Port}");

class Settings
{
    // Initialisers here keep the nullable reference types check happy
    // without making the properties required.
    public string Host { get; set; } = "";
    public int Port { get; set; }
}

// Output:
// localhost:5432 localhost:5432 localhost:5432

A constructor that satisfies required members

You need this whenever a type has both required members and a convenience constructor, which is common once a model grows. Treat the attribute as a promise you are making to the compiler and keep the constructor short enough to check by eye. One extra rule: a constructor that chains to another one carrying the attribute must carry it too.

Example · csharp
using System.Diagnostics.CodeAnalysis;

// Both forms build a complete Person.
Person viaInitialiser = new Person { FirstName = "Ada", LastName = "Lovelace" };
Person viaConstructor = new Person("Grace", "Hopper");

Console.WriteLine($"{viaInitialiser.FirstName} {viaInitialiser.LastName}");
Console.WriteLine($"{viaConstructor.FirstName} {viaConstructor.LastName}");

class Person
{
    public Person()
    {
    }

    // Without this attribute, new Person("Grace", "Hopper") is a compile
    // error: the compiler cannot see that the required members are set.
    // The attribute asserts it. The compiler does not check the claim.
    [SetsRequiredMembers]
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public required string FirstName { get; init; }
    public required string LastName { get; init; }
}

// Output:
// Ada Lovelace
// Grace Hopper

Nested and collection initialisers

This is how a whole small object graph is written as one expression, which keeps test data and seed data readable. Watch the difference between Lines = { ... } and Lines = new List<string> { ... }: the first calls Add on the existing list and works on a get-only property, the second replaces the list and needs a setter.

Example · csharp
Order order = new Order
{
    Reference = "A-1",

    // A nested object initialiser. No separate statement for the address.
    ShipTo = new Address { City = "Leeds", Postcode = "LS1 1AA" },

    // A collection initialiser. Each item is passed to Lines.Add, because
    // Lines has no setter and already holds a list.
    Lines = { "widget", "bolt" }
};

Console.WriteLine($"{order.Reference} to {order.ShipTo.City}: {string.Join(", ", order.Lines)}");

class Order
{
    public required string Reference { get; init; }
    public required Address ShipTo { get; init; }

    public List<string> Lines { get; } = new List<string>();
}

class Address
{
    public required string City { get; init; }
    public required string Postcode { get; init; }
}

// Output:
// A-1 to Leeds: widget, bolt

Discussion

  • Be the first to comment on this lesson.