In the previous post we looked at https://dev.to/ahmedshahjr/should-you-migrate-to-net-10-today-an-honest-lts-decision-guide-5758 Once you target .NET 10, you also get C# 14.
This post starts a new 4-part series: C# 14 & Modern C# Deep Dive. Part 1 is about one small keyword that we will use almost every day: field.
In this post we will learn about:
- The old way: a private backing field just to add one check
- What the
fieldkeyword is and how to enable it - Before and after code, plus lazy and trim examples
The rules and the three gotchas to know before you upgrade
In C# 14,
fieldinside a property accessor means "the backing field the compiler made for this property".You can add validation or normalization to a property without declaring a private field and without writing the
getbody.Watch out for three things: initializers skip the setter,
fieldworks only inside that property, and an existing member namedfieldchanges meaning.
The problem: a private field just to add one check
Here is a property we have all written many times. We want a Price that is never negative. An auto property ({ get; set; }) can't do that, so we add a private field and write both accessors.
// Before/Product.cs
namespace FieldKeywordDemo.Before;
public class Product
{
private decimal _price;
public decimal Price
{
get => _price;
set => _price = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Price must not be negative.");
}
public void ApplyDiscount(decimal percent)
{
// Compiles fine, but skips the check in the setter
_price -= _price * percent / 100;
}
}
This works, but notice two things:
- We wrote a field and a
getbody only to add one line of logic to the setter. -
_priceis visible to the whole class.ApplyDiscountwrites to it directly, so a 150% discount gives a negative price, and the setter never runs.
The feature specification names this second point too: the backing field leaks into the rest of the type, and code can bypass the accessors by accident.
What is the field keyword?
field is a contextual keyword added in C# 14. Inside a property accessor, it refers to the backing field that the compiler creates for that property. You don't declare it, and you don't name it.
A property that uses field is called a field-backed property.
How to enable it:
- Target
net10.0. The default language version for .NET 10 is C# 14, so there is nothing else to set. - You need the .NET 10 SDK or Visual Studio 2026.
Note:
fieldwas a preview feature in C# 13. In C# 14 it is a regular, supported feature. Microsoft says using a newer C# version than your target framework's default is unsupported, so don't setLangVersionto 14 on a .NET 8 project.
Before and after
Here is the same Product with field:
// After/Product.cs
namespace FieldKeywordDemo.After;
public class Product
{
public decimal Price
{
get;
set => field = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Price must not be negative.");
}
public void ApplyDiscount(decimal percent)
{
// There is no private field to reach, so this goes through the setter
Price -= Price * percent / 100;
}
}
What changed:
-
No
_pricefield. The compiler creates the backing field for us. -
get;stays automatic. We mix an auto accessor (get;) with a full accessor (set => ...). Before C# 14 this was not allowed. -
The check can't be skipped from other methods.
ApplyDiscountmust usePrice, so the setter always runs.
Per property, we remove one field declaration and one hand-written accessor. For a class with many validated properties, that adds up.
Two more everyday patterns
Trim and normalize input
Emails, usernames and codes often need cleaning before we store them. This pattern is from the What's new in C# 14 page:
// After/Customer.cs
namespace FieldKeywordDemo.After;
public class Customer
{
public string Email
{
get;
set => field = value?.Trim()?.ToLowerInvariant() ?? string.Empty;
} = string.Empty;
}
The = string.Empty initializer gives the backing field a start value, so the nullable checker is happy. (More on initializers below, because they have a catch.)
Lazy loading
A get-only or expression-bodied property can also use field. With ??=, the value is created on first read and then reused:
// After/CurrencyService.cs
namespace FieldKeywordDemo.After;
public class CurrencyService
{
public string[] Currencies => field ??= LoadCurrencies();
private static string[] LoadCurrencies()
{
Console.WriteLine("Loading currencies...");
return ["USD", "EUR", "GBP"];
}
}
Let's run the demo and see the results:
// Program.cs
using FieldKeywordDemo.After;
var product = new Product { Price = 100m };
product.ApplyDiscount(10);
Console.WriteLine($"Price after discount: {product.Price}");
var customer = new Customer { Email = " Ahmed@Example.COM " };
Console.WriteLine($"Email: '{customer.Email}'");
var service = new CurrencyService();
Console.WriteLine(service.Currencies.Length);
Console.WriteLine(service.Currencies.Length); // "Loading..." prints only once
try
{
product.ApplyDiscount(150);
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.Message);
}
Price after discount: 90
Email: 'ahmed@example.com'
Loading currencies...
3
3
Price must not be negative. (Parameter 'value')
As you can see, "Loading currencies..." prints only once, and the 150% discount now throws instead of saving a negative price.
Note:
field ??= ...is not thread-safe. If two threads read the property at the same time,LoadCurrenciescan run twice. For shared objects (singletons in ASP.NET Core, for example), use theLazytype instead.
The rules you need to know
From the language reference and the feature specification:
-
Mix and match accessors.
{ get; set => ... },{ get => ...; set; }and two full accessors all work. -
Get-only and expression-bodied properties can use
field(like the lazy example). -
{ set; }alone is still not allowed, same as before. -
fieldworks only in property accessors. It is not a keyword in indexers or event accessors, and other methods in the class can't see the backing field. -
Use
nameof(value), notnameof(field), in exceptions.nameof(field)doesn't compile. -
Field-targeted attributes work:
[field: SomeAttribute]goes on the hidden backing field, as with auto properties.
Gotcha 1: initializers don't call the setter
A property initializer writes directly to the backing field. The setter does not run.
// After/Settings.cs
namespace FieldKeywordDemo.After;
public class Settings
{
public bool HasChanges { get; private set; }
public string Theme
{
get;
set
{
field = value;
HasChanges = true;
}
} = "light"; // sets the backing field only, HasChanges stays false
}
Often this is what we want: the default value should not count as a "change". But if your setter validates, the initializer value is not validated.
If you want the setter to run for the start value, assign it in the constructor instead. A constructor assignment calls the setter.
Gotcha 2: a member already named field
This is the breaking change to check when you upgrade. If your class already has a member called field, then inside a property accessor the word field now means the backing field, not your member.
// Legacy/Sensor.cs
namespace FieldKeywordDemo.Legacy;
public class Sensor
{
private double field = 0; // an old member, named "field" by chance
// C# 14: warning CS9258. This now reads a NEW backing field, not the member above.
public double Reading => field;
// Fix: say which one you mean (or rename the member)
public double ReadingFixed => this.field; // or @field
}
Building this sample with the .NET 10 SDK gives:
Legacy\Sensor.cs(8,30): warning CS9258: In language version 14.0, the 'field' keyword binds to a synthesized backing field for the property. To avoid generating a synthesized backing field, and to refer to the existing member, use 'this.field' or '@field' instead.
- The compiler gives warning CS9258 when
fieldwould have meant something else in C# 13. Don't ignore it: the code still compiles, but it reads a different value. - A local variable named
fieldinside an accessor is now error CS9272. Rename it or use@field. - The cleanest fix is to rename the old member.
Tip: before upgrading a big solution, search for the word
fieldused as a name. It is rare, but generated code and old DTOs sometimes have it.
When to keep an explicit backing field
field doesn't replace every private field. Here is a simple way to decide:
flowchart TD
A[Does the property need any logic?] -- No --> B[Auto property: get; set;]
A -- Yes --> C{Do other methods need<br/>the raw value, not the property?}
C -- Yes --> D[Keep an explicit private field]
C -- No --> E{Lazy value read by<br/>many threads?}
E -- Yes --> F[Use the Lazy type]
E -- No --> G[Use the field keyword]
Keep an explicit field when another method must reset or read the raw value without going through the property (for example, a Clear() method that bypasses validation on purpose). The field keyword can't help there.
Key takeaways
-
fieldgives you the compiler-made backing field inside a property accessor, in C# 14 / .NET 10. - Use it for validation, normalization and simple lazy loading, without a private field.
- Initializers skip the setter; constructor assignments don't.
- Fix CS9258 warnings with
this.field,@fieldor a rename. - Keep an explicit field when other methods need the raw value, and use the
Lazytype for thread-safe lazy loading.
This is a small demo for learning, not a full guide to every rule. The feature specification covers structs, nullability and overrides in detail.
The sample code is on GitHub:
Direct link to the sample folder: csharp-14-field-keyword
In the next part (Thursday, October 15) we will look at Extension Members in C# 14: extension properties are finally here.
Do you have a class in your codebase where field would remove a backing field today, or do you prefer to keep the explicit field?
Top comments (0)