You have a perfectly clean auto-property. Then somebody files a bug: negative values are sneaking in. So you crack open the property, add a private backing field, wire up the getter, add validation to the setter, and rename the field because the original name clashed with a parameter three methods down. What started as a one-line fix turned into a five-line refactor that touched nothing conceptually interesting.
C# 14 fixes this with the field contextual keyword. It gives you direct access to the compiler-synthesised backing field inside property accessors, so you can add custom logic to one accessor without writing the boilerplate for the other. It shipped with .NET 10 in November 2025, and it is one of those features that immediately makes you wonder why it took this long.
The problem field solves
Auto-properties are brilliant until you need even a tiny bit of custom behaviour. Consider a Hours property that should reject negative values. Before C# 14, the moment you added validation you had to introduce a backing field and implement both accessors:
public class TimePeriod
{
// Bad — four lines of ceremony for one line of logic
private double _hours;
public double Hours
{
get => _hours;
set => _hours = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}
The backing field leaks into the class scope. Other methods can bypass the setter entirely by writing to _hours directly. You have two symbols representing the same concept, and nothing stops them drifting apart.
How field works
With C# 14, you keep the auto-implemented getter and only write the accessor that needs custom logic:
public class TimePeriod
{
// Good — validation without a manual backing field
public double Hours
{
get;
set => field = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}
The compiler generates a hidden backing field named <Hours>k__BackingField — exactly the same as an auto-property. The get; accessor is auto-implemented by the compiler to return that field. The set accessor uses field to write to it. You get the same IL you would have written by hand, without the noise.
You can use field in either accessor, both accessors, or in an expression-bodied property:
// Only the getter has custom logic
public string DisplayName
{
get => field ?? "Anonymous";
set;
}
// Both accessors use field
public decimal Price
{
get => field;
set => field = Math.Max(0, value);
}
// Expression-bodied property
public string Summary => field ??= ComputeSummary();
The init accessor works identically to set — everywhere the specification mentions set, the same rules apply to init.
Lazy initialisation
The expression-bodied form is particularly elegant for lazy patterns. Before field, lazy initialisation required a nullable backing field and a null check:
// Bad — manual field for lazy init
private AppSettings? _settings;
public AppSettings Settings => _settings ??= LoadSettings();
With field, the backing field disappears from your class:
// Good — lazy init without field declaration
public AppSettings Settings => field ??= LoadSettings();
The compiler infers that the backing field is nullable (more on this in the nullability section) and the getter is null-resilient, meaning the compiler understands that it will never return null even though the backing field might be null. You get no constructor warnings about uninitialised state.
INotifyPropertyChanged and MVVM
The field keyword shines in view model code. The classic MVVM property pattern involves a backing field, a getter, a setter that checks for equality, assigns the field, and raises a change event. It is tedious and error-prone:
public class CustomerViewModel : ViewModelBase
{
// Bad — repetitive MVVM boilerplate
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
if (_name == value) return;
_name = value;
OnPropertyChanged();
}
}
}
With field and a helper method on your base class, this collapses:
public class CustomerViewModel : ViewModelBase
{
// Good — same behaviour, far less noise
public string Name
{
get;
set => Set(ref field, value);
}
}
Where Set is defined in the base class:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool Set<T>(ref T location, T value,
[CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(location, value))
return false;
location = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
}
Note that field can be passed as a ref argument. This is what makes the Set(ref field, value) pattern work — the helper method writes directly to the backing field through the reference.
Property initialisers
Property initialisers assign directly to the backing field, bypassing the setter. This is the same behaviour auto-properties have always had, and it matters when your setter has side effects:
public class SettingsViewModel : ViewModelBase
{
// The initialiser writes to the backing field directly.
// No PropertyChanged event fires for the default value.
public bool IsActive
{
get;
set => Set(ref field, value);
} = true;
}
If you want the setter to run during initialisation, assign the property in the constructor instead:
public SettingsViewModel()
{
IsActive = true; // This calls the setter
}
This distinction is intentional. Initialisers run before the base constructor, at which point calling instance methods would be unsafe.
Nullability and null-resilience
The compiler performs a special analysis to determine whether a property is "null-resilient" — meaning the getter returns a non-null value even when the backing field contains default. If the property is null-resilient, the backing field is treated as nullable internally, but the property's public type remains non-nullable.
This is why the lazy pattern works without warnings:
public class Cache
{
// No constructor warning. The compiler sees that the getter
// always returns a non-null value.
public CacheEntry Entry => field ??= LoadEntry();
}
If the getter simply returns field without any null guard, the property is not null-resilient, and the compiler treats the backing field as non-nullable. You will get constructor warnings if you do not initialise it:
public class Service
{
// Warning: field is not initialised in the constructor
public string Name { get => field; set => field = value; }
}
Breaking changes and naming conflicts
field is a contextual keyword, not a full keyword. It only has special meaning inside property accessor bodies. But if you already have a symbol named field in scope — a class field, a parameter, or a local — you will hit a conflict.
The compiler reports a warning in C# 14 when field as a primary expression binds to the backing field but would have bound to a different symbol in earlier versions. You have two escape hatches:
public class Legacy
{
private int field; // existing member named "field"
public int Value
{
// Use @field to reference the member, not the backing field
get => @field;
set => @field = value;
}
public int Other
{
// Or use this.field to reference the member
get => this.field;
set => this.field = value;
}
}
If you declare a variable named field inside a property accessor, the compiler reports an error — not a warning. Rename the variable or prefix it with @.
Attributes on the backing field
You can target the compiler-generated backing field with attributes using the [field:] prefix, just as you can with auto-properties:
[field: NonSerialized]
public string Computed => field ??= ExpensiveCalculation();
[field: JsonIgnore]
public DateTime CachedTimestamp
{
get => field;
set => field = value.ToUniversalTime();
}
If no accessor uses field, a [field:] attribute is a compile-time error — there is no backing field to attach it to.
What field cannot do
The field keyword is scoped to property accessors. You cannot access it from constructors, methods, or other properties. This is by design — it prevents the very problem it was created to solve (accidental bypassing of property logic). But it also means some patterns still require manual backing fields:
Direct field access outside the property. If you need to read or write the underlying storage from a method without going through the property, you need a manual field. This is common in Dispose patterns or serialisation hooks where you intentionally want to skip validation.
Ref-returning properties. The field keyword is not available in ref-returning properties. These cannot have set accessors, and the use case for a ref-returning auto-property has not been compelling enough to justify the complexity.
Reflection-dependent code. The backing field is named <PropertyName>k__BackingField. If you have code — or a library — that reflects over fields by name, switching from a manual _name field to the field keyword will break it. Entity Framework Core configurations using HasField("_name") are a common example. Use UsePropertyAccessMode(PropertyAccessMode.Field) instead, which does not depend on the field name.
// WARNING
If you use Entity Framework Core's HasField() method with a hard-coded field name in your OnModelCreating configuration, switching that property to use field will cause a runtime failure. The compiler-generated field name will not match your configuration.
Common pitfalls
Mixing field with a manual backing field. If you declare a private field and use field in the same property, you have two fields — the manual one and the compiler-generated one. The compiler will not warn you about this. The property reads and writes the compiler-generated field; your manual field sits unused.
Assuming field is accessible in the constructor. You cannot reference field by name in a constructor. Constructor assignments to the property call the setter (if one exists) or write to the backing field directly (if the property only has a getter). But you cannot write field = value; in a constructor body.
Forgetting that initialisers bypass the setter. If your setter enforces invariants, an initialiser that violates those invariants will succeed silently. The initialiser writes to the backing field directly.
Using field as a lambda parameter name inside a property accessor. This is a compile-time error in C# 14. If you have a LINQ query inside a property getter that uses field as a lambda parameter, rename it:
public bool HasData
{
get => _items.Any(field => field is not null); // Error
}
// Good — rename the parameter
public bool HasData
{
get => _items.Any(item => item is not null);
}
Summary
- The
fieldkeyword gives property accessors direct access to the compiler-synthesised backing field, eliminating the need for manual field declarations. - You can add validation, lazy initialisation, or change notification to a single accessor while leaving the other auto-implemented.
- Property initialisers write to the backing field directly, bypassing the setter — the same behaviour auto-properties have always had.
- The compiler's null-resilience analysis lets lazy patterns work without constructor warnings.
fieldis scoped to property accessors. It cannot be accessed from constructors, methods, or other properties.- Watch out for naming conflicts with existing
fieldsymbols, reflection-dependent code that assumes specific field names, and the distinction between initialisers and constructor assignments.