Introduction
The token field enables you to write a property accessor body without declaring an explicit backing field. The token field is replaced with a compiler-synthesized backing field.
For example, previously, if you wanted to ensure that a string property couldn't be set to null, you had to declare a backing field and implement both accessors:
private string _msg;
public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
You can now simplify your code to:
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
You can declare a body for one or both accessors for a field-backed property.
There's a potential breaking change or confusion reading code in types that also include a symbol named field. You can use @field or this.field to disambiguate between the field keyword and the identifier, or you can rename the current field symbol to provide better distinction.
The above is from Microsoft docs.
💡 Rather than throw new ArgumentNullException consider using a library to validate this property like FluentValidation.
Example validator:
public class MessageValidator : AbstractValidator<INotification>
{
public MessageValidator()
{
RuleFor(x => x.Message)
.NotEmpty()
.MinimumLength(3);
}
}
Starter example 1
Here we have two string properties that should use validation rather than throw an exception.
- value, which comes in calls, is a language extension to ensure the expected format.
- CapitalizeFirstLetter handles empty strings gracefully.
public static partial class StringExtensions
{
extension(string input)
{
public string CapitalizeFirstLetter()
=> string.IsNullOrWhiteSpace(input) ?
input : char.ToUpper(input[0]) + input.AsSpan(1).ToString().ToLower();
}
}
public class Person : IPerson
{
public int Id { get; set; }
public required string FirstName
{
get;
set => field = value.CapitalizeFirstLetter();
}
public required string LastName
{
get;
set => field = value.CapitalizeFirstLetter();
}
}
Starter example 2
Similar to the first example, the string properties are trimmed here. The State property will throw an exception if a user attempts to circumvent validation.
public class Address : IAddress
{
public int Id { get; set; }
public required int CustomerId { get; set; }
public required string Street { get; set => field = value.Trim(); }
public required string City { get; set => field = value.Trim(); }
public required string State
{
get;
set
{
field = value.ToUpper().Trim();
if (!GetStateAbbreviations().Contains(field))
{
throw new ArgumentException("Invalid state abbreviation.");
}
}
}
public required string ZipCode { get; set => field = value.Trim(); }
public required string Country { get; set => field = value.Trim(); }
public required string Phone { get; set => field = value.Trim(); }
}
internal class Helpers
{
/// <summary>
/// Retrieves a list of valid U.S. state abbreviations.
/// </summary>
/// <returns>
/// A <see cref="List{T}"/> of strings containing two-letter state abbreviations.
/// </returns>
public static List<string> GetStateAbbreviations() =>
[
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA",
"HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD",
"MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ",
"NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC",
"SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"
];
}
Intermediate example
This example shows passing the field by ref, along with the user input.
- Note the use of INotifyPropertyChanged and EqualityComparer.
/// <summary>
/// Represents a view model that implements the <see cref="INotifyPropertyChanged"/> interface.
/// </summary>
/// <remarks>
/// This class provides properties with change notification support, enabling data binding scenarios.
/// </remarks>
public sealed class SomeViewModel : INotifyPropertyChanged
{
public string Name
{
get;
set => SetValue(ref field, value);
}
public bool IsActive
{
get;
set => SetValue(ref field, value);
} = true;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
/// <summary>
/// Sets the specified field to the provided value and raises the <see cref="PropertyChanged"/> event if the value changes.
/// </summary>
/// <typeparam name="T">The type of the field and value.</typeparam>
/// <param name="field">A reference to the field to be updated.</param>
/// <param name="value">The new value to assign to the field.</param>
/// <param name="propertyName">
/// The name of the property that changed. This parameter is optional and is automatically provided by the compiler.
/// </param>
/// <remarks>
/// This method ensures that the field is only updated if the new value is different from the current value.
/// It also triggers the <see cref="PropertyChanged"/> event to notify listeners of the change.
/// </remarks>
private void SetValue<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
OnPropertyChanged(propertyName);
}
}
Limitations and Gotchas
-
Accessor bodies only. You can't use
fieldin regular method bodies, constructors, or anywhere outside a property accessor. It's scoped to get, set, and init. - One field per property. Each property gets its own synthesized backing field. You can't share a field between multiple properties.
- Compiler-generated name. The backing field gets a compiler-generated name (similar to auto-properties today). You can't reference it by name in reflection or serialization attributes. If you need control over the field name, use an explicit backing field.
Summary
Basics have been covered for the field keyword, nothing fancy by design, for ease of learning. Also, emphasis on not throwing exceptions unless absolutely necessary, rather than using a validation library.
Top comments (0)