You need a property that validates its input. In C# 13 and earlier, that means writing a private backing field, a get accessor that returns it, and a set accessor that validates the value before storing it. Three moving parts for one property:
public class Greeting
{
private string _msg = "Hello";
public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
}
One property, one validation rule, three lines of ceremony. The backing field _msg exists only to give set something to write to and get something to read from. It carries no meaning of its own.
Multiply this across a configuration class with five or six validated properties, and the noise adds up fast.
Enter the field Keyword
C# 14 introduces field, a contextual keyword you can use inside property accessors to reference the compiler-generated backing field. You write the accessor that needs custom logic. The compiler generates the other one for you, exactly like an auto-property:
public class Greeting
{
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
}
Same validation. No explicit backing field. The get accessor is auto-implemented by the compiler because it has no body, and field in the set accessor points to the compiler-synthesized backing field behind the scenes.
The Rules
fieldis a contextual keyword. It only has special meaning inside a property accessor body. Outside of accessors,fieldis still a regular identifier.- You can use
fieldinget,set, orinitaccessors. Provide a body for one, both, or just the one that needs logic. - You can combine
fieldwith a property initializer:public string Name { get; set => field = value.Trim(); } = "Default";
Practical Examples
Input Validation on Configuration Options
Configuration classes are full of properties that need guard clauses. The field keyword cuts the noise:
public class OAuthClientOptions
{
public string ClientId
{
get;
set => field = !string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("ClientId cannot be empty.", nameof(value));
}
public Uri RedirectUri
{
get;
set => field = value.IsAbsoluteUri
? value
: throw new ArgumentException("RedirectUri must be an absolute URI.", nameof(value));
}
}
Each property enforces its contract in the setter. No backing fields cluttering the class. The get accessors are auto-generated.
Lazy Initialization
The field ??= pattern gives you lazy initialization without Lazy<T> or an explicit backing field:
public class ExpensiveService
{
public Connection DatabaseConnection
{
get => field ??= CreateConnection();
}
private Connection CreateConnection()
{
// Imagine this is expensive
return new Connection("Server=localhost;Database=app");
}
}
public class Connection(string connectionString)
{
public string ConnectionString => connectionString;
}
The first time you read DatabaseConnection, it calls CreateConnection() and stores the result. Every subsequent read returns the cached instance. This works because field starts as null (the default for reference types), and ??= only assigns when the left side is null.
One caveat: this is not thread-safe. If multiple threads hit the getter simultaneously, CreateConnection() could run more than once. For thread-safe lazy initialization, stick with Lazy<T>.
Trimming Input on Set
A simple pattern for cleaning up string input:
public class Person
{
public string? FirstName
{
get;
set => field = value?.Trim();
}
}
Every assignment to FirstName gets trimmed. The caller never needs to remember to trim, and the getter returns the cleaned value without any transformation.
Change Notification (INotifyPropertyChanged)
ViewModels in MVVM apps share the same boilerplate: check whether the value has changed, update the field, and raise the event. The field keyword helps:
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class SettingsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Theme
{
get;
set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
} = "Light";
public int FontSize
{
get;
set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
} = 14;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Compare this with the version that declares _theme and _fontSize fields manually. The field keyword removes two fields and two get accessor bodies. The logic stays, the scaffolding goes.
Disambiguating field
Because field is a contextual keyword, it only has special meaning inside a property accessor. But if your class already has a member or local variable named field, you need to disambiguate:
public class Sensor
{
private string field; // a member named "field" — not ideal, but it happens
public string Reading
{
get;
set
{
// "field" here refers to the compiler-generated backing field
field = value;
// Use this.field to access the instance member named "field"
this.field = value;
}
}
public string Label
{
get;
set
{
// Use @field to refer to a local variable named "field"
var @field = value.ToUpper();
field = @field; // assigns the keyword backing field from the local
}
}
}
The disambiguation rules:
| Syntax | Refers to |
|---|---|
field (inside accessor) |
Compiler-synthesized backing field |
this.field |
Instance member named field |
@field |
Local variable or parameter named field |
The simplest fix: rename the member. If you have a member called field, this is a good time to give it a more descriptive name.
Limitations and Gotchas
A few things to know before you use field everywhere:
- Accessor bodies only. You can't use
fieldin regular method bodies, constructors, or anywhere outside a property accessor. It's scoped toget,set, andinit. - 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.
- Field-targeted attributes work, property-targeted ones don't transfer. You can use the
[field: NonSerialized]attribute target on afield-keyword property, just like with auto-properties. But property-level attributes like[JsonIgnore]affect the property, not the backing field. If you need an attribute that targets the backing field and nofield:target exists for it, you need an explicit backing field. - One field per property. Each property gets its own synthesized backing field. You can't share a field between multiple properties.
Clean Configuration Classes for Identity
The options pattern is everywhere in ASP.NET Core. Classes that configure middleware, authentication handlers, and token validation all benefit from validated properties. Here's what a token validation options class looks like with field:
public class TokenValidationOptions
{
public required string Issuer
{
get;
set => field = !string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("Issuer cannot be empty.", nameof(value));
}
public required string Audience
{
get;
set => field = !string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("Audience cannot be empty.", nameof(value));
}
public TimeSpan ClockSkew
{
get;
set => field = value >= TimeSpan.Zero
? value
: throw new ArgumentOutOfRangeException(nameof(value), "ClockSkew must not be negative.");
} = TimeSpan.FromMinutes(5);
public int MaxTokenLifetimeMinutes
{
get;
set => field = value > 0
? value
: throw new ArgumentOutOfRangeException(nameof(value), "MaxTokenLifetimeMinutes must be positive.");
} = 60;
}
Four validated properties. Zero backing fields. Issuer and Audience are required, so the compiler enforces that callers set them at construction. ClockSkew and MaxTokenLifetimeMinutes have sensible defaults via initializers. Every property enforces its contract at the point of assignment, and any invalid value throws immediately rather than being silently accepted and failing later at runtime.
Configuration classes with validation are everywhere in identity code. Duende IdentityServer uses the options pattern for configuring token lifetimes, issuer URIs, signing credentials, and more. The field keyword makes these classes cleaner without sacrificing the validation that keeps configuration errors from reaching production.
Conclusion
The field keyword solves a narrow problem well: adding logic to a property without the ceremony of an explicit backing field. It won't change how you architect applications, but it will make the properties you write every day a little cleaner. Start using it anywhere you have a backing field that exists only to support a get/set pair.
Thanks for stopping by!
We hope this post helped you on your identity and security journey. If you need a hand with implementation, our docs are always open. For everything else, come hang out with the team and other developers on GitHub.
If you want to get early access to new features and products while collaborating with experts in security and identity standards, join us in our Duende Product Insiders program. And if you prefer your tech content in video form, our YouTube channel is the place to be. Don't forget to like and subscribe!
Questions? Comments? Just want to say hi? Leave a comment below and let's start a conversation.