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

  1. Less Boilerplate: No need to manually declare and name backing fields
  2. Better Readability: The intent is clearer when you see the property logic in one place
  3. 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.