Explore C# 13's preview field keyword that enables direct access to compiler-synthesized backing fields, simplifying property implementations and reducing boilerplate code.
C# 13 introduces the field keyword as a preview feature that fundamentally changes how developers can implement properties. This contextual keyword provides direct access to compiler-synthesized backing fields, enabling cleaner property implementations without the need for explicit field declarations. While still in preview, this feature represents a significant step toward reducing boilerplate code and improving developer productivity.
The field keyword is a contextual keyword that can only be used within property accessors. It provides direct access to the compiler-generated backing field for a property, eliminating the need to manually declare private fields for property implementations. Key characteristics include:
<LangVersion>preview settingPrior to the field keyword, implementing custom property logic required explicit backing field declarations:
public class TraditionalPerson
{
// Explicit backing fields required
private string _firstName;
private string _lastName;
private int _age;
private DateTime _lastModified;
public string FirstName
{
get => _firstName ?? string.Empty;
set
{
if (_firstName != value)
{
_firstName = value?.Trim();
_lastModified = DateTime.UtcNow;
OnPropertyChanged(nameof(FirstName));
}
}
}
public string LastName
{
get => _lastName ?? string.Empty;
set
{
if (_lastName != value)
{
_lastName = value?.Trim();
_lastModified = DateTime.UtcNow;
OnPropertyChanged(nameof(LastName));
}
}
}
public int Age
{
get => _age;
set
{
if (value < 0)
throw new ArgumentException("Age cannot be negative");
if (_age != value)
{
_age = value;
_lastModified = DateTime.UtcNow;
OnPropertyChanged(nameof(Age));
}
}
}
public DateTime LastModified => _lastModified;
// Common issues with explicit backing fields:
// 1. Type mismatches between field and property
private int _heightInches; // int field
public double Height // double property - potential confusion
{
get => _heightInches / 12.0;
set => _heightInches = (int)(value * 12);
}
// 2. Naming convention inconsistencies
private string first_name; // Different naming style
private string m_lastName; // Different prefix
private string lastNameField; // Verbose naming
// 3. Forgetting to use the backing field
private string _email;
public string Email
{
get => _firstName; // Oops! Wrong field
set => _email = value;
}
// 4. Boilerplate for simple validation
private string _phone;
public string Phone
{
get => _phone;
set
{
// Common pattern repeated everywhere
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Phone cannot be empty");
_phone = value;
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
// Property change notification logic
}
}
// Complex scenarios with multiple backing fields
public class TraditionalProduct
{
private string _name;
private decimal _basePrice;
private decimal _discountPercentage;
private bool _isDiscounted;
public string Name
{
get => _name;
set => _name = value?.Trim().ToUpperInvariant();
}
public decimal BasePrice
{
get => _basePrice;
set
{
if (value < 0)
throw new ArgumentException("Price cannot be negative");
_basePrice = value;
UpdateDiscountStatus();
}
}
public decimal DiscountPercentage
{
get => _discountPercentage;
set
{
if (value < 0 || value > 100)
throw new ArgumentException("Discount must be between 0 and 100");
_discountPercentage = value;
UpdateDiscountStatus();
}
}
public decimal FinalPrice => _basePrice * (1 - _discountPercentage / 100);
public bool IsDiscounted => _isDiscounted;
private void UpdateDiscountStatus()
{
_isDiscounted = _discountPercentage > 0;
}
}
With the field keyword, the same functionality becomes much cleaner and more maintainable:
public class ModernPerson
{
private DateTime _lastModified; // Still need explicit field for shared state
// Clean property implementations with field keyword
public string FirstName
{
get => field ?? string.Empty;
set
{
if (field != value)
{
field = value?.Trim();
_lastModified = DateTime.UtcNow;
OnPropertyChanged(nameof(FirstName));
}
}
}
public string LastName
{
get => field ?? string.Empty;
set
{
if (field != value)
{
field = value?.Trim();
_lastModified = DateTime.UtcNow;
OnPropertyChanged(nameof(LastName));
}
}
}
public int Age
{
get => field;
set
{
if (value < 0)
throw new ArgumentException("Age cannot be negative");
if (field != value)
{
field = value;
_lastModified = DateTime.UtcNow;
OnPropertyChanged(nameof(Age));
}
}
}
public DateTime LastModified => _lastModified;
// Benefits of field keyword:
// 1. No type mismatch possible - field is always the right type
public double Height
{
get => field;
set => field = value < 0 ? 0 : value; // field is automatically double
}
// 2. No naming convention concerns
public string Email
{
get => field?.ToLowerInvariant() ?? string.Empty;
set => field = value?.Trim();
}
// 3. Impossible to use wrong field
public string Phone
{
get => field; // Always the correct backing field
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Phone cannot be empty");
field = value;
}
}
// 4. Simplified validation patterns
public string Website
{
get => field;
set => field = string.IsNullOrWhiteSpace(value) ? null :
value.StartsWith("http") ? value : $"https://{value}";
}
protected virtual void OnPropertyChanged(string propertyName)
{
// Property change notification logic
}
}
// Complex scenarios become much cleaner
public class ModernProduct
{
public string Name
{
get => field;
set => field = value?.Trim().ToUpperInvariant();
}
public decimal BasePrice
{
get => field;
set
{
if (value < 0)
throw new ArgumentException("Price cannot be negative");
field = value;
OnPriceChanged();
}
}
public decimal DiscountPercentage
{
get => field;
set
{
if (value < 0 || value > 100)
throw new ArgumentException("Discount must be between 0 and 100");
field = value;
OnPriceChanged();
}
}
// Computed properties remain the same
public decimal FinalPrice => BasePrice * (1 - DiscountPercentage / 100);
public bool IsDiscounted => DiscountPercentage > 0;
private void OnPriceChanged()
{
// Notification logic
}
}
// Advanced usage patterns
public class SmartConfiguration
{
// Lazy initialization with field
public string ConfigValue
{
get
{
if (field == null)
{
field = LoadFromConfiguration("ConfigValue");
}
return field;
}
set => field = value;
}
// Validation with transformation
public string NormalizedEmail
{
get => field;
set => field = value?.Trim().ToLowerInvariant();
}
// Range validation
public int Percentage
{
get => field;
set => field = Math.Max(0, Math.Min(100, value));
}
// Collection property with field
public List<string> Tags
{
get => field ??= new List<string>();
set => field = value ?? new List<string>();
}
private string LoadFromConfiguration(string key)
{
// Simulate configuration loading
return $"DefaultValue_{key}";
}
}
// Pattern for gradual migration from auto-properties
public class GradualMigration
{
// Start with auto-property
public string AutoProperty { get; set; }
// Migrate to field keyword when custom logic is needed
public string CustomProperty
{
get => field?.ToUpper();
set => field = value?.Trim();
}
// Can mix both approaches in the same class
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string FormattedTimestamp
{
get => field ??= Timestamp.ToString("yyyy-MM-dd HH:mm:ss");
set => field = value;
}
}
Here's a practical example showing the field keyword in a data entity with validation and change tracking:
public class Employee : INotifyPropertyChanged
{
// Using field keyword for properties with business logic
public string FirstName
{
get => field ?? string.Empty;
set
{
var normalizedValue = value?.Trim();
if (field != normalizedValue)
{
field = normalizedValue;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName)); // Dependent property
}
}
}
public string LastName
{
get => field ?? string.Empty;
set
{
var normalizedValue = value?.Trim();
if (field != normalizedValue)
{
field = normalizedValue;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName)); // Dependent property
}
}
}
public string Email
{
get => field;
set
{
var normalizedValue = value?.Trim().ToLowerInvariant();
if (!IsValidEmail(normalizedValue))
throw new ArgumentException("Invalid email format");
if (field != normalizedValue)
{
field = normalizedValue;
OnPropertyChanged();
}
}
}
public decimal Salary
{
get => field;
set
{
if (value < 0)
throw new ArgumentException("Salary cannot be negative");
if (field != value)
{
var oldValue = field;
field = value;
OnPropertyChanged();
OnSalaryChanged(oldValue, value);
}
}
}
// Computed property using other field-backed properties
public string FullName => $"{FirstName} {LastName}".Trim();
// Events and helper methods
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler<SalaryChangedEventArgs> SalaryChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnSalaryChanged(decimal oldValue, decimal newValue)
{
SalaryChanged?.Invoke(this, new SalaryChangedEventArgs(oldValue, newValue));
}
private bool IsValidEmail(string email)
{
return !string.IsNullOrEmpty(email) && email.Contains("@");
}
}
public class SalaryChangedEventArgs : EventArgs
{
public decimal OldValue { get; }
public decimal NewValue { get; }
public SalaryChangedEventArgs(decimal oldValue, decimal newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}
Enable collection initialization syntax with the field keyword:
// Suggested future syntax
public List<string> Items
{
get => field ??= [];
set => field = value ?? [];
}
Support for properties that depend on multiple backing fields:
// Suggested future syntax
public string FullName
{
get => $"{field:FirstName} {field:LastName}";
// Auto-updates when either FirstName or LastName field changes
}
Allow attributes on the synthesized backing field:
// Suggested future syntax
[field: NonSerialized]
public string TempData
{
get => field;
set => field = value;
}
Support for generic constraints on field-backed properties:
// Suggested future syntax
public T Value<T> where T : class
{
get => field;
set => field = value ?? throw new ArgumentNullException();
}
Enhanced debugging capabilities:
The field keyword is currently a preview feature that requires:
<LangVersion>preview</LangVersion> in your project fileBe careful when using classes that have an actual field named field:
public class ConflictExample
{
private string field = "actual field"; // This creates a conflict
public string Property
{
get => field; // Refers to the keyword, not the field
set => @field = value; // Use @ to refer to the actual field
// or use this.field = value;
}
}
When migrating from explicit backing fields to the field keyword:
The field keyword in C# 13 represents a significant step toward reducing boilerplate code and improving developer productivity in property implementations. While still in preview, this feature shows great promise for simplifying common property patterns and making C# code more concise and maintainable.
This feature is particularly valuable for:
INotifyPropertyChangedAs this feature moves from preview to full release, we can expect to see refinements based on community feedback and real-world usage. The field keyword represents C#'s continued evolution toward reducing ceremony while maintaining type safety and performance.
Developers should experiment with this feature in non-production code to provide feedback to Microsoft and prepare for its eventual full release. The potential for cleaner, more maintainable property implementations makes this one of the most exciting language enhancements in C# 13, even in its preview state.