init-only setters and required members: object initialisation contracts, constructors, with expressions and deserialisation.
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.
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.
init-only setterspublic 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.
required memberspublic 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).[SetsRequiredMembers]).CompilerFeatureRequired markers stop older compilers from bypassing it).[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.
| 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 |
End > Start, valid email) — a constructor or factory can validate all values together; required only ensures presence.record Money(decimal Amount, string Currency)) are concise for small value objects.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.
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.
The interviewer checks whether you understand modern initialisation features precisely, including their limits.
Green flags
init setters may be called.required is a call-site compiler check and what [SetsRequiredMembers] does.with expressions.Red flags
init gives deep immutability.required validates content or works at run time everywhere.init is encodedAn 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.
required is encoded[RequiredMember]; the type gets [RequiredMember].[SetsRequiredMembers] get [CompilerFeatureRequired("RequiredMembers")] and [Obsolete] with a special message so older compilers refuse to use them.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.
initpublic 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.
System.Text.Json sets init-only properties via reflection or source generation; with required, missing members raise errors (JsonRequiredAttribute equivalent).required (use [JsonProperty(Required = Required.Always)]).IOptions<T>) can bind init-only properties in recent .NET versions; validate options with ValidateDataAnnotations().ValidateOnStart().required members and try creating it without them.[SetsRequiredMembers] constructor and observe the compiler accepting it.System.Text.Json.with on a record with required members.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
}
}
}
required as validationWhat 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.
[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.
required on DI service dependenciesWhat 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.
initWhat happens: DTOs with { get; set; }.
Why it is wrong: objects can be mutated after validation.
Do instead: init for DTOs and messages.
init to stop reflectionWhat 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.
new() constraintsWhat happens: generic factories fail to compile.
Why it is wrong: required members conflict with new().
Do instead: use factory delegates or [SetsRequiredMembers] constructors.
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.
required for domain invariantsWhat 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.
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.
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.
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.
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.
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.
Last updated 30 Oct 2026.