Field Backed Properties in .NET 10

Search for a command to run...

No comments yet. Be the first to comment.
Preparing for the Keys That Outlive Your Algorithms

Microservices can be a sensible response to scale, independent teams and genuinely different deployment needs. They can also be an expensive starting assumption. Imagine an online store that was divid

A modular monolith gives each module clear ownership of its business rules and data. The Orders module controls orders, the Customers module controls customer information, and the Payments module cont

Memory makes an AI assistant useful. It can remember that a customer prefers email, that a project uses a particular naming convention, or that a developer wants examples written with primary construc

Properties have always given C# developers a clean public API over internal state. For a simple value, an auto property keeps the implementation compact:
public string DisplayName { get; set; } = string.Empty;
As soon as the property needs validation, normalisation or change notification, that compact declaration usually expands into a property plus a manually declared field. C# 14, which ships with .NET 10, fills the gap between those two forms with the field contextual keyword. field gives an accessor direct access to that property's compiler-synthesised backing field. We can add logic to get, set or init without declaring storage ourselves.
public string DisplayName
{
get;
set => field = value.Trim();
} = string.Empty;
It is a small language feature, but it removes a surprisingly common piece of repetitive code while keeping the behaviour visible at the property boundary.
field changesBefore .NET 10, a property that rejected null needed an explicit field and implementations for both accessors:
public sealed class UserProfile
{
private string _displayName = string.Empty;
public string DisplayName
{
get => _displayName;
set => _displayName = value
?? throw new ArgumentNullException(nameof(value));
}
}
With a field backed property, the compiler owns the field and we only write the behaviour we need:
public sealed class UserProfile
{
public string DisplayName
{
get;
set => field = value
?? throw new ArgumentNullException(nameof(value));
} = string.Empty;
}
The public property behaves in the same way. The difference is where the storage declaration comes from.
Each field backed property has its own hidden field. field refers only to the field belonging to the property whose accessor is currently executing. It cannot accidentally refer to the backing field of another property. The feature works particularly well when one accessor needs logic and the other remains automatic:
public decimal CreditLimit
{
get;
set => field = value is >= 0 and <= 100_000
? value
: throw new ArgumentOutOfRangeException(
nameof(value),
"The credit limit must be between 0 and 100,000.");
}
Here, get; reads the generated field, while the custom setter validates before assigning to the same field.
Field backed properties are a good fit for small, deterministic transformations that should happen whenever a value enters an object. Trimming a reference, converting a code to a consistent case or clamping a numeric setting can now stay within the property declaration.
public sealed class Submission
{
public required string Reference
{
get;
init => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException(
"A submission reference is required.",
nameof(value))
: value.Trim().ToUpperInvariant();
}
}
The property remains required, and init still limits assignment to object initialisation or construction. The value is checked and normalised once as it enters the object:
var submission = new Submission
{
Reference = " uw-2026-1042 "
};
Console.WriteLine(submission.Reference);
// UW-2026-1042
This is useful for value like properties, but I would keep larger workflows out of an accessor. A property assignment that performs I/O, updates several aggregates or has retry behaviour is difficult to reason about. In those cases, a method or factory still communicates the operation more clearly.
Another common backing field pattern appears in UI and observable models. The setter compares the incoming value with the current value, updates the field, then raises PropertyChanged.
using System.ComponentModel;
using System.Runtime.CompilerServices;
public sealed class SettingsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Theme
{
get;
set
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
if (field == value)
{
return;
}
field = value;
OnPropertyChanged();
}
} = "System";
private void OnPropertyChanged(
[CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName));
}
Previously, Theme would also need a _theme field. The new form keeps all state-specific code together and removes the opportunity to read one field while accidentally writing another. The compiler generated field is still ordinary per-instance storage. field does not introduce observable behaviour on its own; the notification occurs only because the setter explicitly raises it.
field is available in getters as well. That makes it suitable for a small lazy cache where the value should be calculated on first access and then reused:
public sealed class Report
{
public string Content => field ??= BuildContent();
private static string BuildContent()
{
Console.WriteLine("Building report content...");
return "Generated report";
}
}
The compiler supplies the storage needed by the ??= expression. C# 14's nullable analysis understands this pattern: the hidden backing field can begin as null, while the public getter still returns a non-null string.
This example is intentionally small. The property is not thread-safe simply because it uses field; concurrent callers could both run BuildContent. If initialisation must happen once across threads, use Lazy<T>, locking or another explicit concurrency mechanism.
A field backed property can have an
just like an auto property:
public string Region
{
get;
set => field = value.Trim().ToUpperInvariant();
} = "IE";
The initialiser is applied directly to the generated backing field. It does not execute the custom setter. That distinction is useful, but it also means the initialiser should already satisfy the property's rules. In this example, "IE" is already in the normalised form the setter produces. Assignments made through the property, including assignments from a constructor, use its accessor logic. If construction needs to populate storage while deliberately bypassing that logic, an explicit backing field is the clearer option because field is only available inside the property's accessors.
There is no requirement to leave one accessor automatic. Both can contain logic and share the generated field:
public sealed class ApiOptions
{
public Uri BaseAddress
{
get => field;
set
{
ArgumentNullException.ThrowIfNull(value);
field = value.IsAbsoluteUri
? value
: throw new ArgumentException(
"The base address must be absolute.",
nameof(value));
}
} = new("https://api.example.com");
}
Writing get => field; is more verbose than get;, so I would normally use the automatic form unless the getter also needs behaviour. The important point is that field can be used by either or both accessors.
field is contextualfield is a contextual keyword rather than a universally reserved word. Its special meaning applies within property accessors and property expression bodies. Existing code can therefore still contain a member named field, but an unqualified reference inside an accessor can become confusing:
public sealed class LegacyExample
{
private string field = "existing member";
public string Value
{
get => field;
set => this.field = value;
}
}
Those two lines refer to different storage locations. this.field or @field can disambiguate the existing identifier, but renaming the member is usually easier for the next person reading the code.
There are two other scope details worth knowing:
field is available for properties, not indexers or event accessors.
nameof(field) is not supported because the generated field has no source-level name.
Field backed properties remove boilerplate; they do not replace every explicit field. I would retain a named field when code outside the property accessors must access the storage directly, when several properties intentionally share the same storage, or when a framework or reflection based tool depends on a particular field name.
An explicit field is also clearer when synchronisation spans more than a single accessor:
public sealed class TemperatureCache
{
private readonly object _sync = new();
private decimal? _latest;
public decimal Latest
{
get
{
lock (_sync)
{
return _latest ??= ReadTemperature();
}
}
}
public void Invalidate()
{
lock (_sync)
{
_latest = null;
}
}
private static decimal ReadTemperature() => 18.5m;
}
Invalidate needs access to the same storage as the getter, so a named _latest field remains appropriate. There is also a migration detail around metadata. Replacing a named private field with compiler generated storage leaves the property's public API intact, but it changes the private field represented in metadata. Code that reaches into private fields through reflection, specialised serialisers or test helpers can observe that change. Such dependencies are brittle, but they still need checking before a broad automated refactor.
Field backed properties are part of C# 14. A .NET 10 project selects C# 14 by default, so a normal SDK-style project needs no preview switch or explicit language version:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
I prefer allowing the target framework to choose its supported C# version rather than setting LangVersion to latest. It keeps local builds and CI aligned and avoids silently opting older target frameworks into unsupported language combinations.
The safest candidates are properties whose named backing field is used only by that property's accessors. The change is mechanical:
| Existing code | Field backed form |
|---|---|
get => _name; |
get; or get => field; |
_name = value; inside the accessor |
field = value; |
private string _name = ""; |
Property initialiser = ""; |
Other methods access _name |
Keep the explicit field |
After conversion, run the existing tests and search for reflection, serialisation mappings or configuration that refers to the old field by name. For observable objects, test that equal assignments still avoid duplicate events. For validated properties, test the boundary values and confirm the initialiser already follows the same rule.
They solve a narrow problem well. They let a property grow beyond automatic accessors without immediately introducing a separate field and duplicated getter code. Validation, normalisation, change notification and small lazy getters all become more compact, while the property remains the single place where its behaviour is defined. I would use them by default when the storage belongs exclusively to one property and all access can remain inside its accessors. Once other members need the storage, or its identity has significance outside the property, a conventional named field is still the more explicit design.