C# 14 Field-Backed Properties: A Cleaner Way
One of the developer-friendly features in C# 14 (shipping with .NET 10) is field-backed properties. This feature eliminates a common pain point when working with properties that need custom logic while maintaining clean, readable code.
The Problem in C# 13 and Earlier
In previous versions of C#, you had two main options for properties:
Option 1: Auto-Implemented Properties
Simple and clean, but no room for custom logic:
public class User
{
public string Name { get; set; }
}
Option 2: Manual Backing Fields
Full control, but verbose and repetitive:
public class User
{
private string _name;
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
_name = value;
}
}
}
Notice how we had to manually declare _name and reference it in both the getter and setter. This adds boilerplate.
The C# 14 Solution: Field-Backed Properties
C# 14 introduces the field contextual keyword, allowing you to access the compiler-generated backing field directly:
public class User
{
public string Name { get; set; }
{
get => field;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
field = value;
}
}
}
Real-World Examples
Example 1: Input Validation
C# 13 Way:
public class Product
{
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (value < 0)
throw new ArgumentException("Price cannot be negative");
_price = value;
}
}
}
C# 14 Way:
public class Product
{
public decimal Price
{
get => field;
set => field = value >= 0
? value
: throw new ArgumentException("Price cannot be negative");
}
}
Example 2: Lazy Initialization
C# 13 Way:
public class DataService
{
private List<string>? _cache;
public List<string> Cache
{
get
{
if (_cache == null)
_cache = LoadDataFromDatabase();
return _cache;
}
}
}
C# 14 Way:
public class DataService
{
public List<string> Cache
{
get => field ??= LoadDataFromDatabase();
}
}
Example 3: Property Change Notification
C# 13 Way:
public class ViewModel : INotifyPropertyChanged
{
private string _status;
public string Status
{
get => _status;
set
{
if (_status != value)
{
_status = value;
OnPropertyChanged(nameof(Status));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
C# 14 Way:
public class ViewModel : INotifyPropertyChanged
{
public string Status
{
get => field;
set
{
if (field != value)
{
field = value;
OnPropertyChanged(nameof(Status));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Key Benefits
- Less Boilerplate: No need to manually declare and name backing fields
- Better Readability: The intent is clearer when you see the property logic in one place
- Easier Refactoring: Converting from auto-property to custom logic is now seamless
Conclusion
Field-backed properties in C# 14 bridge the gap between auto-properties and manual backing fields, C# continues to prioritize developer productivity without sacrificing power or flexibility.