DEV Community

Ahmed Shah
Ahmed Shah

Posted on

C# 14's field Keyword: Property Validation Without a Backing Field

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 field keyword 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, field inside 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 get body.

  • Watch out for three things: initializers skip the setter, field works only inside that property, and an existing member named field changes 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

This works, but notice two things:

  1. We wrote a field and a get body only to add one line of logic to the setter.
  2. _price is visible to the whole class. ApplyDiscount writes 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: field was 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 set LangVersion to 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

What changed:

  • No _price field. 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. ApplyDiscount must use Price, 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;
}
Enter fullscreen mode Exit fullscreen mode

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"];
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
Price after discount: 90
Email: 'ahmed@example.com'
Loading currencies...
3
3
Price must not be negative. (Parameter 'value')
Enter fullscreen mode Exit fullscreen mode

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, LoadCurrencies can run twice. For shared objects (singletons in ASP.NET Core, for example), use the Lazy type 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.
  • field works 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), not nameof(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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
  • The compiler gives warning CS9258 when field would 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 field inside 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 field used 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

  • field gives 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, @field or a rename.
  • Keep an explicit field when other methods need the raw value, and use the Lazy type 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)