Browse C# 14

C# 14: Field-Backed Properties

C# 14 introduces the field keyword inside property accessors, eliminating the private backing field while guaranteeing that validation logic can never be skipped.

There's also a video on this topic using different examples, and the two complement each other.

C# 14 introduces the field contextual keyword, which lets you write logic inside a property accessor without declaring a separate private backing field. The syntax is cleaner, but the more important benefit is a safety guarantee: the backing storage becomes inaccessible outside the property itself.

The problem

Consider a Thermostat class that exposes a TargetTemperature property. The set accessor validates that the value stays within a safe operating range:

public class Thermostat
{
    private double _targetTemperature;

    public double TargetTemperature
    {
        get => _targetTemperature;
        set => _targetTemperature = value is >= 10 and <= 30
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value), "Temperature must be between 10°C and 30°C.");
    }

    public void FactoryReset() => _targetTemperature = 0;
}

The set accessor enforces the range. But FactoryReset reaches past it and writes 0 directly to the backing field - a value the accessor would have rejected. In other words, the validation has been silently bypassed.

var thermostat = new Thermostat { TargetTemperature = 21 };
Console.WriteLine(thermostat.TargetTemperature); // 21

thermostat.FactoryReset();
Console.WriteLine(thermostat.TargetTemperature); // 0 - out of range, no exception thrown

This is the core issue: private fields are still accessible from anywhere within the same type. The backing field and the accessor that validates it are two separate things, and code inside the class can always choose which one to use.

The C# 14 fix

Replace the backing field and its usages with the field keyword:

public class Thermostat
{
    public double TargetTemperature
    {
        get => field;
        set => field = value is >= 10 and <= 30
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value), "Temperature must be between 10°C and 30°C.");
    }
}

The compiler generates the backing field automatically. Most importantly, field only exists within the property’s accessor body - no other method in the class can access it. Any method that wants to change TargetTemperature now has to go through the property, which means guaranteed validation.

You can also omit field from the get accessor when you just want to return it as-is, which is the case in this example:

public double TargetTemperature
{
    get;
    set => field = value is >= 10 and <= 30
        ? value
        : throw new ArgumentOutOfRangeException(nameof(value), "Temperature must be between 10°C and 30°C.");
}

Both forms compile to the same result.

One naming caveat

If you already have a member named field in your type, the compiler raises a warning due to the naming conflict. In practice this is very uncommon, but still worth knowing if you’re adding this pattern to an existing legacy codebase (which might still have confusing code with confusing naming).

The takeaway

The field keyword isn’t just about removing boilerplate - it actually changes who can reach the backing field. When nothing else in the type can touch it directly, only then can you guarantee that your validation logic is genuinely enforced instead of being respected by convention.

Related reading

  • .NET

    Modern .NET Explained: A Beginner-Friendly Guide Video

    What .NET actually is, how it evolved from .NET Framework to a unified modern platform, and what you need to know to get started without any confusion.

    5 min
  • C#

    C# Tuples: Group Values Without Creating a Type Video

    Tuples let you return multiple values from a method without a custom class or out parameters. Here's the syntax, when to use them, and the difference between Tuple and ValueTuple.

    4 min
  • C# 15

    C# 15: Collection Expression Arguments Video

    C# 15 extends collection expressions with the with keyword, letting you pass constructor arguments like capacity or a comparer directly inside the expression.

    2 min