📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How do init and required work together in C#? Explain init-only setters (C# 9), required members (C# 11), what the compiler enforces, SetsRequiredMembers, how they interact with constructors, object initialisers, with expressions and System.Text.Json, and when you would choose a constructor instead.

init-only setters and required members: object initialisation contracts, constructors, with expressions and deserialisation.

Mid-Level Theory Intermediate immutability init object-initialisers required system-text-json

🎯 Key points

  • init setters can be called only in constructors, object initialisers, with expressions and init accessors
  • required members must be set by every caller, checked by the compiler at each creation site
  • SetsRequiredMembers on a constructor exempts its callers and must be kept honest
  • Object initialisers run after the constructor, so constructor validation cannot see initialised values
  • with expressions copy the object and call init setters on the copy
  • System.Text.Json honours required from .NET 7 and supports init setters
  • init does not make referenced collections or objects immutable
  • Use constructors or factories for cross-property invariants and dependencies

⏱ 30-second answer

An init accessor (C# 9) lets a property be set only during object creation: in the constructor, an object initialiser or a with expression, and never afterwards. A required member (C# 11) forces every caller to set it in the object initialiser, checked by the compiler at each new. Together they give immutable objects with named, mandatory initialisation and no long constructors. A constructor marked [SetsRequiredMembers] tells the compiler it sets everything, so callers using it skip the check. System.Text.Json (.NET 7+) honours required and throws if the property is missing. Choose a constructor instead when you need validation across properties, because object initialisers run after the constructor finishes.

🗣 Full answer (senior)

How I would answer this in an interview

init (C# 9) makes a property settable only during object creation; required (C# 11) makes the compiler force callers to set it during creation. Together they give immutable objects with object-initialiser syntax and compile-time guarantees that essential data is provided. I'd explain what each enforces, [SetsRequiredMembers], interactions with constructors, initialisers, with and System.Text.Json, and when a constructor is better.

1. init-only setters

public sealed class ShippingAddress
{
    public string Street { get; init; } = "";
    public string Suburb { get; init; } = "";
    public string State { get; init; } = "";
    public string Postcode { get; init; } = "";
}

var a = new ShippingAddress { Street = "1 George St", Suburb = "Sydney", State = "NSW", Postcode = "2000" };
// a.State = "VIC";   // CS8852: init-only property can only be assigned in an object initializer, with, or constructor

init accessors can be called from: constructors (and chained constructors) of the type, object initialisers, with expressions, and init accessors of the same object. After construction, the property is read-only.

2. required members

public sealed class CreateCustomerRequest
{
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }
}

var ok = new CreateCustomerRequest { Name = "Ada", Email = "[email protected]" };
var bad = new CreateCustomerRequest { Name = "Ada" };   // CS9035: Required member 'Email' must be set
  • required can be applied to properties and fields (with set or init).
  • The compiler checks every object creation of the type: all required members must be set in the initialiser (unless the constructor used is marked [SetsRequiredMembers]).
  • It works across assemblies (metadata + CompilerFeatureRequired markers stop older compilers from bypassing it).
  • It does not validate the value (empty strings are fine) — it only ensures assignment.

3. [SetsRequiredMembers]

public sealed class Customer
{
    public required string Name { get; init; }
    public required EmailAddress Email { get; init; }

    public Customer() { }

    [SetsRequiredMembers]
    public Customer(string name, EmailAddress email) { Name = name; Email = email; }
}

var c = new Customer("Ada", email);   // no initialiser needed

It tells the compiler "this constructor sets all required members". The compiler trusts it — it doesn't verify — so use it carefully. It also disables required-member checking for that constructor call entirely.

4. Interactions

Feature Behaviour
Constructors Run before the object initialiser; can set init properties; required members still must be set by the initialiser unless [SetsRequiredMembers]
Object initialisers Primary way to set init/required members
with expressions Can set init properties on the copy; required isn't re-checked (the original already had them)
Inheritance Derived types inherit required members; they can't remove required (but can add more)
Generics with new() A type with required members can't satisfy new() unless its parameterless constructor has [SetsRequiredMembers]
System.Text.Json (.NET 7+) Honours required: missing JSON properties throw JsonException; supports init setters natively
EF Core Materialises via constructors or by setting properties (it can set init properties); required isn't a constraint for EF, nullability/configuration is
Model binding / minimal APIs Binding uses System.Text.Json for bodies, so required is enforced there

5. When I choose a constructor instead

  • Invariants across values need validation (End > Start, valid email) — a constructor or factory can validate all values together; required only ensures presence.
  • Domain entities that must never exist in an invalid state, created through intention-revealing factories.
  • Positional records (record Money(decimal Amount, string Currency)) are concise for small value objects.
  • Dependencies (DI services) — constructor injection, not required properties.

I use required + init mainly for DTOs, options and message contracts with many optional and some mandatory fields, where named initialisers are clearer than a long constructor.

6. A production example

A public API accepted CreateOrderRequest with string CustomerId { get; set; }; clients occasionally omitted it and orders were created with a null customer, failing later in fulfilment. Marking it required made System.Text.Json reject the payload with a 400 at the boundary, and C# callers in our own integration services got compile errors until they set it. We kept validation (format checks) in FluentValidation.

🔍 What the interviewer is checking

The interviewer checks whether you understand modern initialisation features precisely, including their limits.

Green flags

  • Knows exactly where init setters may be called.
  • Explains that required is a call-site compiler check and what [SetsRequiredMembers] does.
  • Knows initialisers run after constructors and the impact on validation.
  • Mentions System.Text.Json support and with expressions.

Red flags

  • Says init gives deep immutability.
  • Thinks required validates content or works at run time everywhere.
  • Cannot say when a constructor is still the right choice.

📚 Learn in depth

1. How init is encoded

An init accessor is a setter with modreq(System.Runtime.CompilerServices.IsExternalInit) on its return type. Compilers that don't understand IsExternalInit can't call it (the modreq makes the signature unrecognisable), preventing older languages from mutating init-only properties. Reflection can still set them (PropertyInfo.SetValue) — init is a compile-time guarantee.

2. How required is encoded

  • Members get [RequiredMember]; the type gets [RequiredMember].
  • Constructors without [SetsRequiredMembers] get [CompilerFeatureRequired("RequiredMembers")] and [Obsolete] with a special message so older compilers refuse to use them.
  • The compiler checks object creation expressions against the required member set.

3. Records with required members

public sealed record OrderPlaced
{
    public required Guid OrderId { get; init; }
    public required DateTimeOffset At { get; init; }
}

Nominal records with required give named, immutable message types without long constructors. Positional records already require all parameters via the constructor.

4. Validation patterns with init

public required string Email
{
    get;
    init => field = MailAddress.TryCreate(value, out _) ? value : throw new ArgumentException("Invalid email", nameof(value));
}

C# 14's field keyword makes per-property validation in init concise. Cross-property validation still needs a constructor, factory, or an IValidatableObject-style step.

5. Reflection and serialisers

  • System.Text.Json sets init-only properties via reflection or source generation; with required, missing members raise errors (JsonRequiredAttribute equivalent).
  • Newtonsoft.Json sets init-only properties but doesn't enforce required (use [JsonProperty(Required = Required.Always)]).
  • Configuration binding (IOptions<T>) can bind init-only properties in recent .NET versions; validate options with ValidateDataAnnotations().ValidateOnStart().

Try it yourself

  1. Create a class with required members and try creating it without them.
  2. Add a [SetsRequiredMembers] constructor and observe the compiler accepting it.
  3. Deserialise JSON missing a required property with System.Text.Json.
  4. Use with on a record with required members.

💻 Code

using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

public sealed class Customer
{
    public required string Name { get; init; }
    public required string Email
    {
        get;
        init => field = value.Contains('@') ? value : throw new ArgumentException("Invalid email", nameof(Email)); // C# 14 field keyword
    }
    public string? Phone { get; init; }
    public List<string> Tags { get; init; } = [];   // init does not make the list immutable

    public Customer() { }

    [SetsRequiredMembers]
    public Customer(string name, string email)
    {
        Name = name;
        Email = email;
    }
}

public sealed record Booking
{
    public required DateOnly Start { get; init; }
    public required DateOnly End { get; init; }

    // Cross-property rule: object initialisers run after constructors, so use a factory
    public static Booking Create(DateOnly start, DateOnly end) =>
        end >= start ? new Booking { Start = start, End = end }
                     : throw new ArgumentException("End must not be before Start");
}

public static class Program
{
    public static void Main()
    {
        var mia = new Customer { Name = "Mia", Email = "[email protected]" };
        var leo = new Customer("Leo", "[email protected]");   // ctor satisfies required members
        // mia.Name = "Changed";                                  // CS8852: init-only
        mia.Tags.Add("vip");                                     // allowed: the list itself is mutable

        var booking = Booking.Create(new DateOnly(2026, 12, 1), new DateOnly(2026, 12, 5));
        var extended = booking with { End = new DateOnly(2026, 12, 9) }; // init setters on the copy
        Console.WriteLine($"{leo.Name} {extended}");

        try
        {
            JsonSerializer.Deserialize<Customer>("""{"Email":"[email protected]"}""");
        }
        catch (JsonException ex)
        {
            Console.WriteLine(ex.Message);   // required property 'Name' missing
        }
    }
}

⚠ Common mistakes

1. Treating required as validation

What happens: assuming required string Email guarantees a valid email. Why it is wrong: it only ensures assignment (even "" or, with !, null). Do instead: validate values separately.

2. Misusing [SetsRequiredMembers]

What happens: adding it to constructors that don't set all members. Why it is wrong: the compiler trusts it; objects end up incomplete. Do instead: only use it when the constructor truly sets everything.

3. required on DI service dependencies

What happens: services with required properties for dependencies. Why it is wrong: DI uses constructors; required properties aren't set by the container. Do instead: constructor injection.

4. Public setters instead of init

What happens: DTOs with { get; set; }. Why it is wrong: objects can be mutated after validation. Do instead: init for DTOs and messages.

5. Expecting init to stop reflection

What happens: relying on init-only for security. Why it is wrong: reflection and serialisers can set it. Do instead: treat it as a design guarantee, not a security boundary.

6. Required members on types used with new() constraints

What happens: generic factories fail to compile. Why it is wrong: required members conflict with new(). Do instead: use factory delegates or [SetsRequiredMembers] constructors.

7. Long constructors for DTOs

What happens: DTO constructors with 15 parameters. Why it is wrong: easy to swap arguments of the same type. Do instead: required + init with named initialisers.

8. Relying on required for domain invariants

What happens: domain entities created via initialisers with required members. Why it is wrong: cross-field invariants aren't checked. Do instead: constructors or factory methods that validate.

🏛 Senior insight: trade-offs

init and required turn object initialisers into a checked construction contract, which is ideal for DTOs, options and messages that have many fields and change often. Adding a field with a constructor is a breaking change for every caller; adding a required property is also breaking but the compiler shows exactly which call sites must change, and an optional init property is not breaking at all.

Type of object Preferred construction
API DTO, message, options required + init
Small value object positional record with validation
Domain entity with invariants constructor or factory
Service with dependencies constructor injection

I also mention the limit honestly: required is about presence, not validity. It does not stop empty strings or bad emails, so it complements validation rather than replacing it.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate validates in an init accessor. Why: Checks property accessors and the field keyword.
    How to answer

    A property looks like a field to callers but compiles to get_X/set_X methods, so you can add validation, change storage or compute values without changing callers. Auto-properties (public string Name { get; set; }) get a compiler-generated private backing field. init (C# 9) allows setting only during object creation; required (C# 11) forces callers to set the member in the object initialiser. The C# 14 field keyword lets an accessor use the generated backing field, so set => field = value.Trim(); no longer needs a hand-written field. Use properties for public state, private fields for internal storage, and methods for work that is slow, has side effects or may fail.

  2. When: Candidate mentions with expressions. Why: Records combine init properties with with expressions.
    How to answer

    A record (record class) is a reference type for which the compiler generates value-based equality (Equals, GetHashCode, ==, IEquatable<T>), a virtual EqualityContract so a base and derived record are never equal, ToString via PrintMembers, a copy constructor used by with, and, for positional records, init properties and Deconstruct. with is a shallow copy: nested objects and lists are shared. Equality compares members with EqualityComparer<T>.Default, so a List<T> member is compared by reference, which breaks the "value" idea. Records fit DTOs, messages, events and value objects; they are a poor fit for EF Core entities, which need identity, not value equality, and are tracked by reference.

  3. When: Candidate mentions nullable warnings on uninitialised properties. Why: required replaces = default! in nullable code.
    How to answer

    Nullable reference types are compile-time flow analysis, not a run-time guarantee, so every ! and = default! is a promise the compiler cannot check. In review I remove ! that hides real nulls, replace = default! with required members or constructor parameters, and teach the compiler my helpers' contracts with attributes: [NotNullWhen(true)] on Try methods, [MemberNotNull] on initialisers, [NotNullIfNotNull], [DoesNotReturn] on throw helpers. For generics, T? on an unconstrained T means "may be default". At boundaries the compiler cannot see (JSON, EF Core queries, reflection, old libraries) I validate at run time, because a non-nullable string can still hold null there. With NRT on, EF Core also maps non-nullable reference properties to NOT NULL columns, so annotations change your schema.

🛠 Real-world problems: how you handle them

Missing connection string reached production as null
Situation
A worker service bound a [StorageOptions] class with plain get/set properties from configuration across [dev, test and prod] environments.
Signals
The service started healthy in production but failed on the first queue message with an ArgumentNullException deep inside the storage client.
Investigation
Found that the production key was named [Storage:ConnString] instead of [Storage:ConnectionString]; binding left the property null and nothing checked it.
Resolution
Made critical option properties `required` with `init`, added DataAnnotations plus `ValidateOnStart()`, and fixed the key.
Prevention
Options validation on start for every options class, plus a deployment smoke test that starts the service with production configuration.

How to tell it (STAR)

**Situation:** our [ingestion worker] started fine in production but crashed on the first message. **Task:** I needed to fix it and make configuration mistakes fail fast. **Action:** I found a misnamed configuration key that left a property null. I changed our options classes to use `required` and `init` so code creating them had to set every critical value, and added start-up validation so binding problems stopped the service immediately. **Result:** the fix shipped in [an hour], and since then configuration mistakes fail at start-up with a clear message instead of at [2 am] on the first message.

↩ Usually asked after

More in Modern C#

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me