A Brief Discussion on C# Property Syntactic Sugar and NRT Mechanism Evolution
TLDR
- Property initializers and expression-bodied properties differ in their lifecycle; the former executes only once during object initialization, while the latter executes every time it is called. Misuse can lead to logical errors.
- The
fieldkeyword introduced in C# 14 resolves the pain point of having to revert to manual backing fields for simple logic (such asTrim()) in auto-properties. - The NRT (Nullable Reference Types) mechanism must be paired with
<WarningsAsErrors>nullable</WarningsAsErrors>to achieve mandatory enforcement. requiredandinitsyntax solve the issue where DTOs had to be written with meaninglessnull!default values under the NRT mechanism.- Using
[SetsRequiredMembers]resolves compilation conflicts between parameterized constructors andrequiredproperties.
C# Property Syntax Evolution and Pitfalls
The evolution of C# properties aims to simplify definitions, but different syntaxes have significant differences in execution timing. If not clarified, they can easily trigger bugs.
Difference Between Property Initializers and Expression-bodied Properties
When you might encounter this issue: Confusing "static caching" and "dynamic calculation" syntax when defining read-only properties or setting initial values.
public string Name => "Default"(Expression-bodied): Re-executes every time it is called.public string Name { get; } = "Default"(Property Initializer): Executes only once when the object is instantiated (new).
WARNING
Disaster Scenario: If you use public Guid OrderId => Guid.NewGuid();, a brand new Guid will be generated every time the property is read, causing serialization or log tracking to fail. You should use public Guid CorrectOrderId { get; } = Guid.NewGuid(); instead to ensure a constant state.
Simplifying Property Logic with the field Keyword
When you might encounter this issue: When you need to add simple logic (such as Trim() or NotifyPropertyChanged) to an auto-property but do not want to write a verbose backing field.
The field keyword introduced in C# 14 allows direct access to the implicit field generated by the compiler:
public class User {
public string Name {
get;
set => field = value.Trim();
}
}TIP
It is recommended to place logic processing in the set accessor to avoid the overhead of frequent calls in get and to reduce potential side effects when frameworks like Entity Framework Core access the backing field directly.
NRT (Nullable Reference Types) and Mechanism Completion
The core of NRT lies in explicitly declaring the nullability of reference types using ?. To enforce this contract, you should set the following in your project file:
<WarningsAsErrors>nullable</WarningsAsErrors>Solving the Initialization Dilemma for DTOs under NRT
When you might encounter this issue: During the C# 8.0 to 10.0 era, non-null properties had to be initialized, forcing DTOs to be written with null! or empty strings to bypass the compiler, resulting in code noise.
Through the init and required keywords, you can ensure that properties are immutable after initialization and force the caller to provide a value, eliminating the need for meaningless default values:
public class UserDto {
public required string UserName { get; init; }
}
// The caller must provide a value, otherwise compilation fails
UserDto dto = new() { UserName = "Alice" };Handling Conflicts Between Constructors and required
When you might encounter this issue: When a class defines both a parameterized constructor and required properties, the compiler will issue a warning because it cannot perform initialization via { }.
Using the [SetsRequiredMembers] attribute informs the compiler that the constructor has completed the assignment of all required members:
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 ASP.NET Core's [FromBody] serialization, if a property is marked as required, the system will automatically throw a JsonException if the frontend fails to pass that field. This helps clearly distinguish the semantic difference between "passing a default value" and "not passing a field at all."
Change Log
- 2026-03-30 Initial version created.
