A Brief Discussion on C# Property Syntax Sugar and NRT Mechanism Evolution
TLDR
fieldkeyword (C# 14): Allows direct access to the compiler-generated hidden field in auto-properties, enabling logic injection without reverting to manual backing fields.- Avoid confusion between
=>and{ get; } =:=>is dynamically calculated and executes on every call;{ get; } =is a static cache that executes only once during object initialization. - NRT usability improvements: Through
initandrequiredkeywords, the struggle of being forced to usenull!or meaningless default values for DTOs under the NRT mechanism is resolved. - Enforced compilation checks: By setting
<WarningsAsErrors>nullable</WarningsAsErrors>, NRT warnings can be elevated to compilation errors, ensuring consistent team contracts. - Collaboration between constructors and
required: Using the[SetsRequiredMembers]attribute informs the compiler that the constructor has completed the assignment of required members, resolving conflicts withrequired.
The Evolution of C# Property Syntax
C# property syntax has undergone several evolutions aimed at simplifying definitions and reducing redundant code.
1. Early Classical Approach (Backing Field)
In the C# 1.0 era, properties required manual declaration of private fields to store data.
public class User {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}2. Auto-Implemented Properties
Introduced in C# 3.0, the compiler automatically generates the underlying field, suitable for data containers that do not require additional logic.
public class User {
public string Name { get; set; }
}3. Property Initializers
C# 6.0 allows assigning initial values directly when defining auto-properties.
public class User {
public string Name { get; set; } = "Default Name";
}4. Expression-bodied Properties
C# 6.0 and 7.0 introduced the => syntax, making property definitions more concise.
WARNING
When you might encounter issues: Confusing the execution timing of Expression-bodied (=>) and Property Initializer ({ get; } =).
public string Name => "Default Name": Recalculated every time it is read.public string Name { get; } = "Default Name": Executed only once when the object is instantiated.
Incorrect Example:
public class Order {
// Incorrect: Generates a new Guid every time it is read, causing issues with serialization or log tracking
public Guid OrderId => Guid.NewGuid();
// Correct: Generated only once during new()
public Guid CorrectOrderId { get; } = Guid.NewGuid();
}5. Semi-Auto Properties and the field Keyword
When you might encounter issues: When an auto-property needs to include minor logic in the set accessor (such as Trim() or change notification), one previously had to revert to a manually written backing field.
C# 14 introduces the field keyword to access the compiler-generated field directly:
public class User {
public string Name {
get;
set => field = value.Trim();
}
}TIP
It is recommended to prioritize logic processing in the set accessor to avoid the overhead of frequent calls in get, and to reduce potential issues when frameworks like Entity Framework Core access the backing field directly.
NRT (Nullable Reference Types) and Completion of Checking Mechanisms
NRT aims to explicitly declare whether a reference type can be null via ? annotation. To enforce this, you can set <WarningsAsErrors>nullable</WarningsAsErrors> in your project file.
Why was it often disabled in the past?
During the C# 8.0 to 10.0 era, if a DTO property was non-nullable, it had to provide a default value or use null! to deceive the compiler, which caused class definitions to bear contracts that could not be guaranteed.
Completion of the Mechanism
Through the following syntax, the development experience of NRT has been significantly improved:
init(C# 9.0): Ensures that properties can only be assigned during initialization, maintaining immutability.required(C# 11.0): Forces the caller to assign a value duringnew(), eliminating the need to writenull!inside the class.
public class UserDto {
public required string UserName { get; init; }
}
// The caller must provide a value, otherwise compilation fails
UserDto dto = new() { UserName = "Alice" };[SetsRequiredMembers]: Resolving Conflicts with Constructors
When you might encounter issues: When a class contains both a custom constructor and required properties, the compiler will issue a warning because it cannot be initialized via { }.
using System.Diagnostics.CodeAnalysis;
public class User {
public required string UserName { get; init; }
[SetsRequiredMembers]
public User(string userName) {
UserName = userName;
}
}Application of required in Web API
In System.Text.Json serialization, if a property is marked as required but the frontend fails to send it, the system will throw a JsonException. This helps distinguish between "the frontend passed a default value" and "the frontend missed the field."
Change Log
- 2026-03-30 Initial document creation.